summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBob Van Landuyt <bob@vanlanduyt.co>2017-10-10 17:53:42 +0200
committerBob Van Landuyt <bob@vanlanduyt.co>2017-10-10 17:53:42 +0200
commit741fb49378abbf66fbd8d6ad27b94f1040bf3123 (patch)
tree6bc2393e7fd3d8733e0ef8a19c98bd16d2b4fdbd
parente678f312923faf9a702e19894175d4cb14f66b5b (diff)
parent9ac5338b8eb361927ad068486398b92acb0c287e (diff)
downloadgitlab-ce-741fb49378abbf66fbd8d6ad27b94f1040bf3123.tar.gz
Merge branch 'master' into bvl-group-trees
-rw-r--r--.gitlab-ci.yml24
-rw-r--r--CHANGELOG.md21
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock16
-rw-r--r--app/assets/images/auth_buttons/signin_with_google.pngbin0 -> 3983 bytes
-rw-r--r--app/assets/images/icon_image_comment.svg1
-rw-r--r--app/assets/images/icon_image_comment@2x.svg1
-rw-r--r--app/assets/images/icons.json2
-rw-r--r--app/assets/images/icons.svg2
-rw-r--r--app/assets/images/new_nav.pngbin14322 -> 0 bytes
-rw-r--r--app/assets/images/old_nav.pngbin25617 -> 0 bytes
-rw-r--r--app/assets/javascripts/abuse_reports.js5
-rw-r--r--app/assets/javascripts/ajax_loading_spinner.js5
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js5
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js3
-rw-r--r--app/assets/javascripts/blob/viewer/index.js2
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js2
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js2
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js21
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js2
-rw-r--r--app/assets/javascripts/build_artifacts.js50
-rw-r--r--app/assets/javascripts/build_variables.js16
-rw-r--r--app/assets/javascripts/ci_lint_editor.js7
-rw-r--r--app/assets/javascripts/clusters.js112
-rw-r--r--app/assets/javascripts/commit.js12
-rw-r--r--app/assets/javascripts/commit/file.js14
-rw-r--r--app/assets/javascripts/commit/image_file.js13
-rw-r--r--app/assets/javascripts/commits.js51
-rw-r--r--app/assets/javascripts/create_label.js29
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/banner.vue55
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js5
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue2
-rw-r--r--app/assets/javascripts/diff.js8
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js9
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js2
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js2
-rw-r--r--app/assets/javascripts/dispatcher.js31
-rw-r--r--app/assets/javascripts/dropzone_input.js5
-rw-r--r--app/assets/javascripts/environments/components/environment.vue2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js7
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js7
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js5
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js2
-rw-r--r--app/assets/javascripts/flash.js145
-rw-r--r--app/assets/javascripts/gl_dropdown.js6
-rw-r--r--app/assets/javascripts/groups/index.js4
-rw-r--r--app/assets/javascripts/header.js19
-rw-r--r--app/assets/javascripts/image_diff/helpers/badge_helper.js38
-rw-r--r--app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js58
-rw-r--r--app/assets/javascripts/image_diff/helpers/dom_helper.js44
-rw-r--r--app/assets/javascripts/image_diff/helpers/index.js25
-rw-r--r--app/assets/javascripts/image_diff/helpers/utils_helper.js95
-rw-r--r--app/assets/javascripts/image_diff/image_badge.js23
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js143
-rw-r--r--app/assets/javascripts/image_diff/init_discussion_tab.js12
-rw-r--r--app/assets/javascripts/image_diff/replaced_image_diff.js92
-rw-r--r--app/assets/javascripts/image_diff/view_types.js9
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js4
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js2
-rw-r--r--app/assets/javascripts/issue.js4
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue5
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue1
-rw-r--r--app/assets/javascripts/job.js (renamed from app/assets/javascripts/build.js)106
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js2
-rw-r--r--app/assets/javascripts/jobs/job_details_mediator.js8
-rw-r--r--app/assets/javascripts/label_manager.js3
-rw-r--r--app/assets/javascripts/labels_select.js3
-rw-r--r--app/assets/javascripts/lib/utils/csrf.js4
-rw-r--r--app/assets/javascripts/lib/utils/image_utility.js5
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js14
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js20
-rw-r--r--app/assets/javascripts/line_highlighter.js8
-rw-r--r--app/assets/javascripts/locale/index.js1
-rw-r--r--app/assets/javascripts/main.js31
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js2
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js7
-rw-r--r--app/assets/javascripts/milestone.js3
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue42
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue59
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue14
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue5
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue (renamed from app/assets/javascripts/monitoring/components/graph_path.vue)0
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js22
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js5
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js2
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js1
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js4
-rw-r--r--app/assets/javascripts/notes.js142
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue37
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue19
-rw-r--r--app/assets/javascripts/notes/components/issue_note.vue5
-rw-r--r--app/assets/javascripts/notes/components/issue_note_awards_list.vue3
-rw-r--r--app/assets/javascripts/notes/components/issue_note_form.vue20
-rw-r--r--app/assets/javascripts/notes/components/issue_notes_app.vue2
-rw-r--r--app/assets/javascripts/notes/mixins/issuable_state.js15
-rw-r--r--app/assets/javascripts/notes/stores/actions.js10
-rw-r--r--app/assets/javascripts/notifications_dropdown.js2
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js3
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js3
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediatior.js3
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue146
-rw-r--r--app/assets/javascripts/profile/account/index.js21
-rw-r--r--app/assets/javascripts/profile/profile.js2
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js51
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js5
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js5
-rw-r--r--app/assets/javascripts/repo/components/repo.vue2
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue2
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue16
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue9
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue41
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js7
-rw-r--r--app/assets/javascripts/repo/services/repo_service.js1
-rw-r--r--app/assets/javascripts/repo/stores/repo_store.js8
-rw-r--r--app/assets/javascripts/search.js2
-rw-r--r--app/assets/javascripts/shortcuts.js15
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js3
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue16
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue61
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue50
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue120
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js6
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js82
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js3
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js1
-rw-r--r--app/assets/javascripts/single_file_diff.js7
-rw-r--r--app/assets/javascripts/star.js3
-rw-r--r--app/assets/javascripts/task_list.js3
-rw-r--r--app/assets/javascripts/two_factor_auth.js3
-rw-r--r--app/assets/javascripts/u2f/authenticate.js188
-rw-r--r--app/assets/javascripts/u2f/error.js43
-rw-r--r--app/assets/javascripts/u2f/register.js151
-rw-r--r--app/assets/javascripts/u2f/util.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js41
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/popup_dialog.vue6
-rw-r--r--app/assets/javascripts/vue_shared/mixins/issuable.js9
-rw-r--r--app/assets/stylesheets/framework.scss4
-rw-r--r--app/assets/stylesheets/framework/animations.scss3
-rw-r--r--app/assets/stylesheets/framework/blocks.scss10
-rw-r--r--app/assets/stylesheets/framework/buttons.scss26
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss8
-rw-r--r--app/assets/stylesheets/framework/header.scss595
-rw-r--r--app/assets/stylesheets/framework/images.scss1
-rw-r--r--app/assets/stylesheets/framework/layout.scss4
-rw-r--r--app/assets/stylesheets/framework/modal.scss13
-rw-r--r--app/assets/stylesheets/framework/new-nav.scss404
-rw-r--r--app/assets/stylesheets/framework/new-sidebar.scss10
-rw-r--r--app/assets/stylesheets/framework/secondary-navigation-elements.scss (renamed from app/assets/stylesheets/framework/nav.scss)335
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss6
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/framework/tooltips.scss7
-rw-r--r--app/assets/stylesheets/framework/variables.scss22
-rw-r--r--app/assets/stylesheets/pages/boards.scss1
-rw-r--r--app/assets/stylesheets/pages/builds.scss8
-rw-r--r--app/assets/stylesheets/pages/clusters.scss9
-rw-r--r--app/assets/stylesheets/pages/commits.scss5
-rw-r--r--app/assets/stylesheets/pages/diff.scss168
-rw-r--r--app/assets/stylesheets/pages/environments.scss19
-rw-r--r--app/assets/stylesheets/pages/issuable.scss30
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/pages/note_form.scss41
-rw-r--r--app/assets/stylesheets/pages/notes.scss25
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss8
-rw-r--r--app/assets/stylesheets/pages/profile.scss13
-rw-r--r--app/assets/stylesheets/pages/search.scss1
-rw-r--r--app/assets/stylesheets/pages/tree.scss8
-rw-r--r--app/controllers/admin/application_controller.rb14
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/application_controller.rb15
-rw-r--r--app/controllers/boards/issues_controller.rb2
-rw-r--r--app/controllers/concerns/notes_actions.rb11
-rw-r--r--app/controllers/confirmations_controller.rb7
-rw-r--r--app/controllers/dashboard/todos_controller.rb30
-rw-r--r--app/controllers/google_api/authorizations_controller.rb29
-rw-r--r--app/controllers/profiles/emails_controller.rb27
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb2
-rw-r--r--app/controllers/projects/artifacts_controller.rb18
-rw-r--r--app/controllers/projects/clusters_controller.rb136
-rw-r--r--app/controllers/projects/git_http_client_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/projects/lfs_api_controller.rb18
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb7
-rw-r--r--app/controllers/projects/notes_controller.rb9
-rw-r--r--app/controllers/registrations_controller.rb29
-rw-r--r--app/controllers/sessions_controller.rb41
-rw-r--r--app/finders/merge_request_target_project_finder.rb18
-rw-r--r--app/helpers/compare_helper.rb4
-rw-r--r--app/helpers/groups_helper.rb9
-rw-r--r--app/helpers/lazy_image_tag_helper.rb1
-rw-r--r--app/helpers/merge_requests_helper.rb3
-rw-r--r--app/helpers/notes_helper.rb4
-rw-r--r--app/helpers/numbers_helper.rb11
-rw-r--r--app/helpers/projects_helper.rb1
-rw-r--r--app/helpers/system_note_helper.rb4
-rw-r--r--app/mailers/emails/profile.rb6
-rw-r--r--app/models/ci/artifact_blob.rb26
-rw-r--r--app/models/ci/build.rb5
-rw-r--r--app/models/ci/build_trace_section.rb11
-rw-r--r--app/models/ci/build_trace_section_name.rb11
-rw-r--r--app/models/ci/pipeline.rb20
-rw-r--r--app/models/concerns/avatarable.rb2
-rw-r--r--app/models/concerns/cache_markdown_field.rb14
-rw-r--r--app/models/concerns/discussion_on_diff.rb4
-rw-r--r--app/models/concerns/has_status.rb1
-rw-r--r--app/models/concerns/issuable.rb29
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/concerns/repository_mirroring.rb25
-rw-r--r--app/models/concerns/routable.rb6
-rw-r--r--app/models/concerns/token_authenticatable.rb4
-rw-r--r--app/models/diff_discussion.rb2
-rw-r--r--app/models/diff_note.rb14
-rw-r--r--app/models/discussion.rb4
-rw-r--r--app/models/email.rb12
-rw-r--r--app/models/fork_network.rb15
-rw-r--r--app/models/fork_network_member.rb7
-rw-r--r--app/models/gcp/cluster.rb113
-rw-r--r--app/models/gpg_key.rb20
-rw-r--r--app/models/gpg_key_subkey.rb22
-rw-r--r--app/models/gpg_signature.rb33
-rw-r--r--app/models/issue.rb18
-rw-r--r--app/models/legacy_diff_discussion.rb8
-rw-r--r--app/models/merge_request.rb33
-rw-r--r--app/models/merge_request_diff.rb1
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/note.rb16
-rw-r--r--app/models/project.rb37
-rw-r--r--app/models/project_services/chat_message/base_message.rb10
-rw-r--r--app/models/project_services/chat_message/issue_message.rb6
-rw-r--r--app/models/project_services/chat_message/merge_message.rb4
-rw-r--r--app/models/project_services/chat_message/note_message.rb4
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb6
-rw-r--r--app/models/project_services/chat_message/push_message.rb8
-rw-r--r--app/models/project_services/chat_message/wiki_page_message.rb4
-rw-r--r--app/models/sent_notification.rb6
-rw-r--r--app/models/system_note_metadata.rb2
-rw-r--r--app/models/user.rb69
-rw-r--r--app/policies/gcp/cluster_policy.rb12
-rw-r--r--app/policies/issuable_policy.rb12
-rw-r--r--app/policies/note_policy.rb2
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/presenters/ci/pipeline_presenter.rb11
-rw-r--r--app/presenters/gcp/cluster_presenter.rb9
-rw-r--r--app/serializers/cluster_entity.rb6
-rw-r--r--app/serializers/cluster_serializer.rb7
-rw-r--r--app/serializers/commit_entity.rb2
-rw-r--r--app/serializers/group_entity.rb2
-rw-r--r--app/serializers/issue_entity.rb3
-rw-r--r--app/serializers/merge_request_entity.rb1
-rw-r--r--app/serializers/pipeline_entity.rb8
-rw-r--r--app/services/ci/create_cluster_service.rb15
-rw-r--r--app/services/ci/extract_sections_from_build_trace_service.rb30
-rw-r--r--app/services/ci/fetch_gcp_operation_service.rb17
-rw-r--r--app/services/ci/fetch_kubernetes_token_service.rb72
-rw-r--r--app/services/ci/finalize_cluster_creation_service.rb33
-rw-r--r--app/services/ci/integrate_cluster_service.rb26
-rw-r--r--app/services/ci/provision_cluster_service.rb36
-rw-r--r--app/services/ci/update_cluster_service.rb22
-rw-r--r--app/services/emails/base_service.rb7
-rw-r--r--app/services/emails/confirm_service.rb7
-rw-r--r--app/services/emails/create_service.rb4
-rw-r--r--app/services/emails/destroy_service.rb4
-rw-r--r--app/services/issuable_base_service.rb14
-rw-r--r--app/services/issues/base_service.rb14
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/keys/last_used_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb14
-rw-r--r--app/services/merge_requests/refresh_service.rb2
-rw-r--r--app/services/notification_service.rb7
-rw-r--r--app/services/projects/destroy_service.rb7
-rw-r--r--app/services/projects/fork_service.rb20
-rw-r--r--app/services/projects/unlink_fork_service.rb1
-rw-r--r--app/services/quick_actions/interpret_service.rb2
-rw-r--r--app/services/system_note_service.rb7
-rw-r--r--app/services/todo_service.rb9
-rw-r--r--app/services/users/activity_service.rb2
-rw-r--r--app/views/admin/groups/_group.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/jobs/index.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml2
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_account.html.haml16
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_account.text.erb14
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_secondary.html.haml8
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_secondary.text.erb7
-rw-r--r--app/views/devise/mailer/confirmation_instructions.html.haml17
-rw-r--r--app/views/devise/mailer/confirmation_instructions.text.erb10
-rw-r--r--app/views/discussions/_diff_discussion.html.haml16
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml31
-rw-r--r--app/views/discussions/_notes.html.haml19
-rw-r--r--app/views/events/event/_push.html.haml3
-rw-r--r--app/views/groups/_home_panel.html.haml2
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/milestones/_header_title.html.haml3
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml6
-rw-r--r--app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml8
-rw-r--r--app/views/notify/new_email_email.html.haml10
-rw-r--r--app/views/notify/new_email_email.text.erb7
-rw-r--r--app/views/profiles/accounts/show.html.haml20
-rw-r--r--app/views/profiles/emails/index.html.haml16
-rw-r--r--app/views/profiles/gpg_keys/_key.html.haml9
-rw-r--r--app/views/projects/_home_panel.html.haml14
-rw-r--r--app/views/projects/_md_preview.html.haml7
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml15
-rw-r--r--app/views/projects/clusters/_form.html.haml37
-rw-r--r--app/views/projects/clusters/_header.html.haml14
-rw-r--r--app/views/projects/clusters/_sidebar.html.haml7
-rw-r--r--app/views/projects/clusters/login.html.haml16
-rw-r--r--app/views/projects/clusters/new.html.haml9
-rw-r--r--app/views/projects/clusters/show.html.haml70
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml15
-rw-r--r--app/views/projects/diffs/_image_diff_frame.html.haml5
-rw-r--r--app/views/projects/diffs/_replaced_image_diff.html.haml61
-rw-r--r--app/views/projects/diffs/_single_image_diff.html.haml16
-rw-r--r--app/views/projects/diffs/viewers/_image.html.haml70
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml4
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml2
-rw-r--r--app/views/projects/jobs/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/tree/_old_tree_content.html.haml2
-rw-r--r--app/views/projects/wikis/empty.html.haml2
-rw-r--r--app/views/shared/_email_with_badge.html.haml (renamed from app/views/profiles/gpg_keys/_email_with_badge.html.haml)4
-rw-r--r--app/views/shared/builds/_tabs.html.haml8
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/members/_group.html.haml2
-rw-r--r--app/views/shared/notes/_form.html.haml35
-rw-r--r--app/views/shared/notes/_note.html.haml15
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml14
-rw-r--r--app/views/users/_groups.html.haml2
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/workers/build_finished_worker.rb1
-rw-r--r--app/workers/build_trace_sections_worker.rb8
-rw-r--r--app/workers/cluster_provision_worker.rb10
-rw-r--r--app/workers/concerns/cluster_queue.rb10
-rw-r--r--app/workers/concerns/project_start_import.rb9
-rw-r--r--app/workers/repository_fork_worker.rb3
-rw-r--r--app/workers/repository_import_worker.rb3
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb27
-rw-r--r--changelogs/unreleased/18608-lock-issues.yml4
-rw-r--r--changelogs/unreleased/23888-fix-unsubscription-link-for-snippet-notification.yml5
-rw-r--r--changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml5
-rw-r--r--changelogs/unreleased/34102-online-view-of-artifacts-fe.yml5
-rw-r--r--changelogs/unreleased/34284-add-changes-to-issuable-webhook-data.yml5
-rw-r--r--changelogs/unreleased/34841-todos.yml5
-rw-r--r--changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml5
-rw-r--r--changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml5
-rw-r--r--changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml5
-rw-r--r--changelogs/unreleased/37229-mr-widget-status-icon.yml5
-rw-r--r--changelogs/unreleased/37467-helper-method-from-users-endpoint-overrides-api-helper-method.yml5
-rw-r--r--changelogs/unreleased/37483-activity-log-show-wrong-number-of-commits-per-push.yml5
-rw-r--r--changelogs/unreleased/37552-replace-js-true-with-js.yml5
-rw-r--r--changelogs/unreleased/37970-ci-sections-tracking.yml5
-rw-r--r--changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml6
-rw-r--r--changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml5
-rw-r--r--changelogs/unreleased/38389-allow-merge-without-success.yml6
-rw-r--r--changelogs/unreleased/38476-improve-merge-jid-cleanup-on-merge-process.yml5
-rw-r--r--changelogs/unreleased/38528-build-url.yml5
-rw-r--r--changelogs/unreleased/38534-minigraph.yml5
-rw-r--r--changelogs/unreleased/38582-popover-badge.yml5
-rw-r--r--changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml5
-rw-r--r--changelogs/unreleased/38775-scrollable-tabs-on-admin.yml5
-rw-r--r--changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml5
-rw-r--r--changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml5
-rw-r--r--changelogs/unreleased/adjusting-tooltips.yml5
-rw-r--r--changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-close-issuable-link.yml5
-rw-r--r--changelogs/unreleased/bvl-fork-network-schema.yml5
-rw-r--r--changelogs/unreleased/commit-side-by-side-comment.yml5
-rw-r--r--changelogs/unreleased/content-title-link-hover-bg.yml5
-rw-r--r--changelogs/unreleased/declarative-policy-optimisations.yml5
-rw-r--r--changelogs/unreleased/dm-api-unauthorized.yml5
-rw-r--r--changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml5
-rw-r--r--changelogs/unreleased/feature-verify_secondary_emails.yml5
-rw-r--r--changelogs/unreleased/fix-kubectl-180.yml5
-rw-r--r--changelogs/unreleased/fix-mr-sidebar-counter-after-merge.yml5
-rw-r--r--changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml5
-rw-r--r--changelogs/unreleased/fl-fix-ca-time-component.yml5
-rw-r--r--changelogs/unreleased/fork-btn-enabled-user-groups.yml5
-rw-r--r--changelogs/unreleased/gitaly_feature_flag_metadata.yml5
-rw-r--r--changelogs/unreleased/group-milestones-breadcrumb.yml5
-rw-r--r--changelogs/unreleased/group-sort-dropdown-blank.yml5
-rw-r--r--changelogs/unreleased/issue_35873.yml5
-rw-r--r--changelogs/unreleased/jobs-sort-by-id.yml5
-rw-r--r--changelogs/unreleased/mk-normalize-ldap-user-dns.yml5
-rw-r--r--changelogs/unreleased/rc-fix-gh-import-branches-performance.yml5
-rw-r--r--changelogs/unreleased/remove_repo_prefix_from_api.yml5
-rw-r--r--changelogs/unreleased/replace_project_merge_requests-feature.yml5
-rw-r--r--changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml5
-rw-r--r--changelogs/unreleased/sh-fix-import-repos.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-38646.yml5
-rw-r--r--changelogs/unreleased/sh-fix-username-logging.yml5
-rw-r--r--changelogs/unreleased/sh-restore-all-refs-backup.yml5
-rw-r--r--changelogs/unreleased/sh-show-all-slack-names.yml5
-rw-r--r--changelogs/unreleased/tc-geo-read-only-idea.yml5
-rw-r--r--changelogs/unreleased/valid-branch-name-dash-bug.yml5
-rw-r--r--changelogs/unreleased/winh-delete-account-modal.yml5
-rw-r--r--changelogs/unreleased/zj-repo-gitaly.yml5
-rw-r--r--config/application.rb3
-rw-r--r--config/gitlab.yml.example1
-rw-r--r--config/initializers/1_settings.rb19
-rw-r--r--config/initializers/devise.rb2
-rw-r--r--config/initializers/doorkeeper_openid_connect.rb2
-rw-r--r--config/initializers/secret_token.rb2
-rw-r--r--config/initializers/sentry.rb4
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/google_api.rb5
-rw-r--r--config/routes/profile.rb9
-rw-r--r--config/routes/project.rb10
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--config/webpack.config.js1
-rw-r--r--db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb1
-rw-r--r--db/migrate/20160716115711_add_queued_at_to_ci_builds.rb1
-rw-r--r--db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb1
-rw-r--r--db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb1
-rw-r--r--db/migrate/20170815221154_add_discussion_locked_to_issuable.rb13
-rw-r--r--db/migrate/20170904092148_add_email_confirmation.rb33
-rw-r--r--db/migrate/20170909090114_add_email_confirmation_index.rb36
-rw-r--r--db/migrate/20170924094327_create_gcp_clusters.rb45
-rw-r--r--db/migrate/20170927161718_create_gpg_key_subkeys.rb23
-rw-r--r--db/migrate/20170928124105_create_fork_networks.rb28
-rw-r--r--db/migrate/20170928133643_create_fork_network_members.rb26
-rw-r--r--db/migrate/20170929080234_add_failure_reason_to_pipelines.rb9
-rw-r--r--db/migrate/20170929131201_populate_fork_networks.rb30
-rw-r--r--db/migrate/20171006090001_create_ci_build_trace_sections.rb19
-rw-r--r--db/migrate/20171006090010_add_build_foreign_key_to_ci_build_trace_sections.rb15
-rw-r--r--db/migrate/20171006090100_create_ci_build_trace_section_names.rb19
-rw-r--r--db/migrate/20171006091000_add_name_foreign_key_to_ci_build_trace_sections.rb15
-rw-r--r--db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb29
-rw-r--r--db/post_migrate/20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys.rb28
-rw-r--r--db/schema.rb99
-rw-r--r--doc/administration/auth/ldap.md4
-rw-r--r--doc/administration/job_artifacts.md6
-rw-r--r--doc/api/issues.md57
-rw-r--r--doc/api/merge_requests.md11
-rw-r--r--doc/development/README.md6
-rw-r--r--doc/development/fe_guide/style_guide_js.md30
-rw-r--r--doc/development/i18n/externalization.md296
-rw-r--r--doc/development/i18n/img/crowdin-editor.pngbin0 -> 88701 bytes
-rw-r--r--doc/development/i18n/index.md76
-rw-r--r--doc/development/i18n/translation.md64
-rw-r--r--doc/development/i18n_guide.md305
-rw-r--r--doc/development/ux_guide/components.md31
-rw-r--r--[-rwxr-xr-x]doc/development/ux_guide/img/illustration-size-large-horizontal.pngbin55272 -> 55272 bytes
-rw-r--r--[-rwxr-xr-x]doc/development/ux_guide/img/illustration-size-medium.pngbin20994 -> 20994 bytes
-rw-r--r--[-rwxr-xr-x]doc/development/ux_guide/img/illustrations-border-radius.pngbin7779 -> 7779 bytes
-rw-r--r--[-rwxr-xr-x]doc/development/ux_guide/img/illustrations-caps-do.pngbin3775 -> 3775 bytes
-rw-r--r--[-rwxr-xr-x]doc/development/ux_guide/img/illustrations-caps-don't.pngbin3922 -> 3922 bytes
-rw-r--r--[-rwxr-xr-x]doc/development/ux_guide/img/illustrations-color-grey.pngbin251 -> 251 bytes
-rw-r--r--[-rwxr-xr-x]doc/development/ux_guide/img/illustrations-color-orange.pngbin275 -> 275 bytes
-rw-r--r--[-rwxr-xr-x]doc/development/ux_guide/img/illustrations-color-purple.pngbin275 -> 275 bytes
-rw-r--r--[-rwxr-xr-x]doc/development/ux_guide/img/illustrations-geometric.pngbin5057 -> 5057 bytes
-rw-r--r--[-rwxr-xr-x]doc/development/ux_guide/img/illustrations-palette-oragne.pngbin10439 -> 10439 bytes
-rw-r--r--[-rwxr-xr-x]doc/development/ux_guide/img/illustrations-palette-purple.pngbin10002 -> 10002 bytes
-rw-r--r--doc/development/ux_guide/img/popover-placement-above.pngbin0 -> 68451 bytes
-rw-r--r--doc/development/ux_guide/img/popover-placement-below.pngbin0 -> 63368 bytes
-rw-r--r--doc/development/verifying_database_capabilities.md12
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/raketasks/backup_restore.md14
-rw-r--r--doc/update/10.0-to-10.1.md356
-rw-r--r--doc/update/mysql_to_postgresql.md297
-rw-r--r--doc/user/discussions/img/discussion_lock_system_notes.pngbin0 -> 50200 bytes
-rw-r--r--doc/user/discussions/img/lock_form_member.pngbin0 -> 100581 bytes
-rw-r--r--doc/user/discussions/img/lock_form_non_member.pngbin0 -> 37432 bytes
-rw-r--r--doc/user/discussions/img/turn_off_lock.pngbin0 -> 31580 bytes
-rw-r--r--doc/user/discussions/img/turn_on_lock.pngbin0 -> 34839 bytes
-rw-r--r--doc/user/discussions/index.md40
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/import/github.md5
-rw-r--r--doc/user/project/integrations/webhooks.md104
-rw-r--r--doc/user/project/issues/deleting_issues.md11
-rw-r--r--doc/user/project/issues/img/delete_issue.pngbin0 -> 49894 bytes
-rw-r--r--doc/user/project/issues/index.md4
-rw-r--r--doc/user/project/pipelines/img/job_artifacts_browser.pngbin3771 -> 3944 bytes
-rw-r--r--doc/user/project/pipelines/job_artifacts.md8
-rw-r--r--doc/user/project/repository/gpg_signed_commits/index.md4
-rw-r--r--features/project/merge_requests.feature324
-rw-r--r--features/steps/project/fork.rb4
-rw-r--r--features/steps/project/forked_merge_requests.rb5
-rw-r--r--features/steps/project/merge_requests.rb632
-rw-r--r--features/support/env.rb2
-rw-r--r--lib/api/branches.rb20
-rw-r--r--lib/api/commits.rb18
-rw-r--r--lib/api/entities.rb45
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/issues.rb3
-rw-r--r--lib/api/merge_requests.rb8
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/tags.rb12
-rw-r--r--lib/api/templates.rb8
-rw-r--r--lib/api/users.rb10
-rw-r--r--lib/api/v3/branches.rb8
-rw-r--r--lib/api/v3/commits.rb16
-rw-r--r--lib/api/v3/entities.rb4
-rw-r--r--lib/api/v3/merge_requests.rb4
-rw-r--r--lib/api/v3/repositories.rb4
-rw-r--r--lib/api/v3/tags.rb4
-rw-r--r--lib/api/v3/templates.rb8
-rw-r--r--lib/banzai/renderer.rb7
-rw-r--r--lib/declarative_policy/rule.rb20
-rw-r--r--lib/declarative_policy/runner.rb31
-rw-r--r--lib/github/import.rb46
-rw-r--r--lib/github/representation/branch.rb20
-rw-r--r--lib/github/representation/issuable.rb12
-rw-r--r--lib/github/representation/issue.rb20
-rw-r--r--lib/github/representation/pull_request.rb75
-rw-r--r--lib/gitlab/background_migration/create_fork_network_memberships_range.rb65
-rw-r--r--lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb53
-rw-r--r--lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb313
-rw-r--r--lib/gitlab/background_migration/populate_fork_networks_range.rb59
-rw-r--r--lib/gitlab/ci/ansi2html.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/config.rb2
-rw-r--r--lib/gitlab/ci/stage/seed.rb2
-rw-r--r--lib/gitlab/ci/trace.rb6
-rw-r--r--lib/gitlab/ci/trace/section_parser.rb97
-rw-r--r--lib/gitlab/ci/trace/stream.rb17
-rw-r--r--lib/gitlab/closing_issue_extractor.rb3
-rw-r--r--lib/gitlab/data_builder/push.rb2
-rw-r--r--lib/gitlab/database.rb9
-rw-r--r--lib/gitlab/diff/file.rb29
-rw-r--r--lib/gitlab/diff/formatters/base_formatter.rb61
-rw-r--r--lib/gitlab/diff/formatters/image_formatter.rb43
-rw-r--r--lib/gitlab/diff/formatters/text_formatter.rb49
-rw-r--r--lib/gitlab/diff/image_point.rb23
-rw-r--r--lib/gitlab/diff/position.rb90
-rw-r--r--lib/gitlab/ee_compat_check.rb6
-rw-r--r--lib/gitlab/gcp/model.rb13
-rw-r--r--lib/gitlab/git/repository.rb4
-rw-r--r--lib/gitlab/git_access.rb9
-rw-r--r--lib/gitlab/git_access_wiki.rb5
-rw-r--r--lib/gitlab/git_ref_validator.rb2
-rw-r--r--lib/gitlab/gitaly_client.rb21
-rw-r--r--lib/gitlab/gitaly_client/namespace_service.rb39
-rw-r--r--lib/gitlab/gpg.rb15
-rw-r--r--lib/gitlab/gpg/commit.rb10
-rw-r--r--lib/gitlab/gpg/invalid_gpg_signature_updater.rb4
-rw-r--r--lib/gitlab/hook_data/issuable_builder.rb56
-rw-r--r--lib/gitlab/hook_data/issue_builder.rb55
-rw-r--r--lib/gitlab/hook_data/merge_request_builder.rb62
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/import_export/relation_factory.rb2
-rw-r--r--lib/gitlab/ldap/auth_hash.rb4
-rw-r--r--lib/gitlab/ldap/dn.rb301
-rw-r--r--lib/gitlab/ldap/person.rb24
-rw-r--r--lib/gitlab/middleware/read_only.rb88
-rw-r--r--lib/gitlab/o_auth/user.rb2
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/gitlab/regex.rb4
-rw-r--r--lib/gitlab/shell.rb50
-rw-r--r--lib/gitlab/usage_data.rb1
-rw-r--r--lib/gitlab/workhorse.rb46
-rw-r--r--lib/google_api/auth.rb54
-rw-r--r--lib/google_api/cloud_platform/client.rb88
-rw-r--r--lib/rspec_flaky/config.rb21
-rw-r--r--lib/rspec_flaky/flaky_example.rb21
-rw-r--r--lib/rspec_flaky/flaky_examples_collection.rb37
-rw-r--r--lib/rspec_flaky/listener.rb63
-rw-r--r--lib/system_check/app/git_user_default_ssh_config_check.rb4
-rw-r--r--lib/tasks/gitlab/dev.rake5
-rw-r--r--lib/tasks/gitlab/gitaly.rake7
-rw-r--r--lib/tasks/import.rake27
-rw-r--r--locale/gitlab.pot238
-rw-r--r--qa/Gemfile.lock8
-rw-r--r--qa/README.md30
-rw-r--r--qa/qa.rb9
-rw-r--r--qa/qa/page/dashboard/groups.rb23
-rw-r--r--qa/qa/page/group/new.rb23
-rw-r--r--qa/qa/page/group/show.rb18
-rw-r--r--qa/qa/runtime/namespace.rb4
-rw-r--r--qa/qa/runtime/user.rb2
-rw-r--r--qa/qa/scenario/gitlab/group/create.rb27
-rw-r--r--qa/qa/scenario/gitlab/project/create.rb20
-rw-r--r--qa/qa/scenario/gitlab/sandbox/prepare.rb28
-rw-r--r--rubocop/cop/migration/datetime.rb20
-rw-r--r--rubocop/cop/rspec/verbose_include_metadata.rb74
-rw-r--r--rubocop/rubocop.rb1
-rw-r--r--spec/controllers/admin/users_controller_spec.rb2
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb26
-rw-r--r--spec/controllers/google_api/authorizations_controller_spec.rb49
-rw-r--r--spec/controllers/profiles/emails_controller_spec.rb35
-rw-r--r--spec/controllers/profiles_controller_spec.rb14
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb70
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb7
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb2
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb308
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb10
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb18
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb70
-rw-r--r--spec/controllers/projects_controller_spec.rb25
-rw-r--r--spec/controllers/registrations_controller_spec.rb64
-rw-r--r--spec/factories/ci/build_trace_section_names.rb6
-rw-r--r--spec/factories/ci/pipelines.rb1
-rw-r--r--spec/factories/emails.rb2
-rw-r--r--spec/factories/fork_networks.rb5
-rw-r--r--spec/factories/gcp/cluster.rb38
-rw-r--r--spec/factories/gpg_key_subkeys.rb10
-rw-r--r--spec/factories/gpg_keys.rb4
-rw-r--r--spec/factories/gpg_signature.rb2
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/projects.rb2
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb2
-rw-r--r--spec/features/admin/admin_broadcast_messages_spec.rb2
-rw-r--r--spec/features/admin/admin_disables_two_factor_spec.rb2
-rw-r--r--spec/features/admin/admin_groups_spec.rb10
-rw-r--r--spec/features/admin/admin_health_check_spec.rb2
-rw-r--r--spec/features/admin/admin_hooks_spec.rb2
-rw-r--r--spec/features/admin/admin_labels_spec.rb2
-rw-r--r--spec/features/admin/admin_projects_spec.rb8
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb2
-rw-r--r--spec/features/admin/admin_users_spec.rb4
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/auto_deploy_spec.rb4
-rw-r--r--spec/features/boards/boards_spec.rb2
-rw-r--r--spec/features/boards/keyboard_shortcut_spec.rb2
-rw-r--r--spec/features/boards/new_issue_spec.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb2
-rw-r--r--spec/features/ci_lint_spec.rb2
-rw-r--r--spec/features/container_registry_spec.rb2
-rw-r--r--spec/features/copy_as_gfm_spec.rb2
-rw-r--r--spec/features/cycle_analytics_spec.rb2
-rw-r--r--spec/features/dashboard/active_tab_spec.rb2
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb2
-rw-r--r--spec/features/dashboard/group_spec.rb8
-rw-r--r--spec/features/dashboard/issues_spec.rb8
-rw-r--r--spec/features/dashboard/label_filter_spec.rb2
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb9
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb2
-rw-r--r--spec/features/dashboard/projects_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_filtering_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb8
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb2
-rw-r--r--spec/features/explore/new_menu_spec.rb6
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb2
-rw-r--r--spec/features/group_variables_spec.rb2
-rw-r--r--spec/features/groups/labels/subscription_spec.rb2
-rw-r--r--spec/features/groups_spec.rb6
-rw-r--r--spec/features/issuables/default_sort_order_spec.rb18
-rw-r--r--spec/features/issuables/discussion_lock_spec.rb106
-rw-r--r--spec/features/issuables/user_sees_sidebar_spec.rb4
-rw-r--r--spec/features/issues/award_emoji_spec.rb18
-rw-r--r--spec/features/issues/award_spec.rb2
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb2
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb2
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb2
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb2
-rw-r--r--spec/features/issues/issue_detail_spec.rb3
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb8
-rw-r--r--spec/features/issues/markdown_toolbar_spec.rb2
-rw-r--r--spec/features/issues/move_spec.rb6
-rw-r--r--spec/features/issues/spam_issues_spec.rb2
-rw-r--r--spec/features/issues/todo_spec.rb2
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb2
-rw-r--r--spec/features/issues_spec.rb40
-rw-r--r--spec/features/login_spec.rb8
-rw-r--r--spec/features/merge_requests/assign_issues_spec.rb2
-rw-r--r--spec/features/merge_requests/award_spec.rb2
-rw-r--r--spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb2
-rw-r--r--spec/features/merge_requests/cherry_pick_spec.rb2
-rw-r--r--spec/features/merge_requests/closes_issues_spec.rb2
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb2
-rw-r--r--spec/features/merge_requests/create_new_mr_from_fork_spec.rb89
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb2
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb23
-rw-r--r--spec/features/merge_requests/deleted_source_branch_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb2
-rw-r--r--spec/features/merge_requests/diffs_spec.rb6
-rw-r--r--spec/features/merge_requests/discussion_lock_spec.rb49
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb4
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb8
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb16
-rw-r--r--spec/features/merge_requests/form_spec.rb12
-rw-r--r--spec/features/merge_requests/image_diff_notes.rb196
-rw-r--r--spec/features/merge_requests/merge_commit_message_toggle_spec.rb2
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb6
-rw-r--r--spec/features/merge_requests/pipelines_spec.rb2
-rw-r--r--spec/features/merge_requests/resolve_outdated_diff_discussions.rb2
-rw-r--r--spec/features/merge_requests/target_branch_spec.rb2
-rw-r--r--spec/features/merge_requests/toggle_whitespace_changes_spec.rb2
-rw-r--r--spec/features/merge_requests/toggler_behavior_spec.rb2
-rw-r--r--spec/features/merge_requests/update_merge_requests_spec.rb6
-rw-r--r--spec/features/merge_requests/user_posts_diff_notes_spec.rb33
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb2
-rw-r--r--spec/features/merge_requests/versions_spec.rb2
-rw-r--r--spec/features/merge_requests/widget_deployments_spec.rb2
-rw-r--r--spec/features/merge_requests/widget_spec.rb21
-rw-r--r--spec/features/profile_spec.rb44
-rw-r--r--spec/features/profiles/emails_spec.rb71
-rw-r--r--spec/features/profiles/gpg_keys_spec.rb14
-rw-r--r--spec/features/profiles/keys_spec.rb2
-rw-r--r--spec/features/profiles/oauth_applications_spec.rb2
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb2
-rw-r--r--spec/features/profiles/user_changes_notified_of_own_activity_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_notifications_tab_spec.rb2
-rw-r--r--spec/features/projects/artifacts/browse_spec.rb50
-rw-r--r--spec/features/projects/badges/list_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_line_permalink_updater_spec.rb2
-rw-r--r--spec/features/projects/blobs/edit_spec.rb2
-rw-r--r--spec/features/projects/blobs/shortcuts_blob_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb4
-rw-r--r--spec/features/projects/clusters_spec.rb111
-rw-r--r--spec/features/projects/commit/builds_spec.rb2
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb2
-rw-r--r--spec/features/projects/compare_spec.rb2
-rw-r--r--spec/features/projects/developer_views_empty_project_instructions_spec.rb4
-rw-r--r--spec/features/projects/edit_spec.rb2
-rw-r--r--spec/features/projects/environments/environments_spec.rb2
-rw-r--r--spec/features/projects/features_visibility_spec.rb4
-rw-r--r--spec/features/projects/files/browse_files_spec.rb2
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb2
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb2
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb2
-rw-r--r--spec/features/projects/files/template_type_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/undo_template_spec.rb2
-rw-r--r--spec/features/projects/gfm_autocomplete_load_spec.rb2
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb2
-rw-r--r--spec/features/projects/issuable_templates_spec.rb10
-rw-r--r--spec/features/projects/jobs_spec.rb6
-rw-r--r--spec/features/projects/labels/subscription_spec.rb2
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb10
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb2
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb2
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb19
-rw-r--r--spec/features/projects/merge_requests/user_closes_merge_request_spec.rb21
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_commit_spec.rb19
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_diff_spec.rb172
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb50
-rw-r--r--spec/features/projects/merge_requests/user_creates_merge_request_spec.rb32
-rw-r--r--spec/features/projects/merge_requests/user_edits_merge_request_spec.rb25
-rw-r--r--spec/features/projects/merge_requests/user_manages_subscription_spec.rb32
-rw-r--r--spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb22
-rw-r--r--spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb63
-rw-r--r--spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb15
-rw-r--r--spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb15
-rw-r--r--spec/features/projects/merge_requests/user_views_diffs_spec.rb46
-rw-r--r--spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb15
-rw-r--r--spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb92
-rw-r--r--spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb72
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb14
-rw-r--r--spec/features/projects/project_settings_spec.rb4
-rw-r--r--spec/features/projects/ref_switcher_spec.rb2
-rw-r--r--spec/features/projects/settings/integration_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb2
-rw-r--r--spec/features/projects/show_project_spec.rb2
-rw-r--r--spec/features/projects/user_browses_files_spec.rb8
-rw-r--r--spec/features/projects/user_creates_directory_spec.rb4
-rw-r--r--spec/features/projects/user_creates_files_spec.rb12
-rw-r--r--spec/features/projects/user_creates_project_spec.rb2
-rw-r--r--spec/features/projects/user_deletes_files_spec.rb6
-rw-r--r--spec/features/projects/user_edits_files_spec.rb44
-rw-r--r--spec/features/projects/user_interacts_with_stars_spec.rb2
-rw-r--r--spec/features/projects/user_replaces_files_spec.rb6
-rw-r--r--spec/features/projects/user_uploads_files_spec.rb13
-rw-r--r--spec/features/projects/view_on_env_spec.rb2
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb2
-rw-r--r--spec/features/projects_spec.rb60
-rw-r--r--spec/features/protected_branches_spec.rb25
-rw-r--r--spec/features/protected_tags_spec.rb2
-rw-r--r--spec/features/snippets/internal_snippet_spec.rb2
-rw-r--r--spec/features/tags/master_creates_tag_spec.rb2
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb6
-rw-r--r--spec/features/task_lists_spec.rb10
-rw-r--r--spec/features/triggers_spec.rb2
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb16
-rw-r--r--spec/features/users/snippets_spec.rb2
-rw-r--r--spec/features/users_spec.rb2
-rw-r--r--spec/features/variables_spec.rb2
-rw-r--r--spec/finders/merge_request_target_project_finder_spec.rb54
-rw-r--r--spec/finders/merge_requests_finder_spec.rb12
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json11
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request.json1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issues.json1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json1
-rw-r--r--spec/fixtures/trace/trace_with_sections15
-rw-r--r--spec/helpers/application_helper_spec.rb8
-rw-r--r--spec/helpers/groups_helper_spec.rb45
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb7
-rw-r--r--spec/helpers/projects_helper_spec.rb2
-rw-r--r--spec/initializers/secret_token_spec.rb18
-rw-r--r--spec/javascripts/abuse_reports_spec.js80
-rw-r--r--spec/javascripts/ajax_loading_spinner_spec.js4
-rw-r--r--spec/javascripts/clusters_spec.js79
-rw-r--r--spec/javascripts/commits_spec.js108
-rw-r--r--spec/javascripts/cycle_analytics/banner_spec.js41
-rw-r--r--spec/javascripts/cycle_analytics/total_time_component_spec.js58
-rw-r--r--spec/javascripts/fixtures/clusters.rb34
-rw-r--r--spec/javascripts/flash_spec.js269
-rw-r--r--spec/javascripts/header_spec.js73
-rw-r--r--spec/javascripts/image_diff/helpers/badge_helper_spec.js132
-rw-r--r--spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js139
-rw-r--r--spec/javascripts/image_diff/helpers/dom_helper_spec.js118
-rw-r--r--spec/javascripts/image_diff/helpers/utils_helper_spec.js207
-rw-r--r--spec/javascripts/image_diff/image_badge_spec.js84
-rw-r--r--spec/javascripts/image_diff/image_diff_spec.js361
-rw-r--r--spec/javascripts/image_diff/init_discussion_tab_spec.js37
-rw-r--r--spec/javascripts/image_diff/mock_data.js28
-rw-r--r--spec/javascripts/image_diff/replaced_image_diff_spec.js312
-rw-r--r--spec/javascripts/image_diff/view_types_spec.js24
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js8
-rw-r--r--spec/javascripts/job_spec.js (renamed from spec/javascripts/build_spec.js)43
-rw-r--r--spec/javascripts/lib/utils/image_utility_spec.js32
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js10
-rw-r--r--spec/javascripts/monitoring/graph/deployment_spec.js25
-rw-r--r--spec/javascripts/monitoring/graph/flag_spec.js49
-rw-r--r--spec/javascripts/monitoring/graph_path_spec.js2
-rw-r--r--spec/javascripts/monitoring/graph_spec.js24
-rw-r--r--spec/javascripts/notes_spec.js4
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js19
-rw-r--r--spec/javascripts/profile/account/components/delete_account_modal_spec.js129
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js16
-rw-r--r--spec/javascripts/repo/components/repo_sidebar_spec.js64
-rw-r--r--spec/javascripts/sidebar/lock/edit_form_buttons_spec.js36
-rw-r--r--spec/javascripts/sidebar/lock/edit_form_spec.js41
-rw-r--r--spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js71
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js109
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js53
-rw-r--r--spec/javascripts/u2f/register_spec.js120
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js111
-rw-r--r--spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js1
-rw-r--r--spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js20
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_warning_spec.js46
-rw-r--r--spec/lib/banzai/renderer_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb117
-rw-r--r--spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb32
-rw-r--r--spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb36
-rw-r--r--spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb93
-rw-r--r--spec/lib/gitlab/checks/force_push_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/stage/seed_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/trace/section_parser_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb87
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb4
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb2
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/formatters/image_formatter_spec.rb20
-rw-r--r--spec/lib/gitlab/diff/formatters/text_formatter_spec.rb42
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb85
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb16
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb4
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb6
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb24
-rw-r--r--spec/lib/gitlab/git/tag_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_spec.rb27
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb29
-rw-r--r--spec/lib/gitlab/git_ref_validator_spec.rb5
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb4
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb39
-rw-r--r--spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb49
-rw-r--r--spec/lib/gitlab/gpg_spec.rb17
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb2
-rw-r--r--spec/lib/gitlab/hook_data/issuable_builder_spec.rb105
-rw-r--r--spec/lib/gitlab/hook_data/issue_builder_spec.rb46
-rw-r--r--spec/lib/gitlab/hook_data/merge_request_builder_spec.rb62
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml9
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/merge_request_parser_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml29
-rw-r--r--spec/lib/gitlab/ldap/auth_hash_spec.rb24
-rw-r--r--spec/lib/gitlab/ldap/dn_spec.rb224
-rw-r--r--spec/lib/gitlab/ldap/person_spec.rb33
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb16
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb142
-rw-r--r--spec/lib/gitlab/search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/shell_spec.rb56
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb1
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb70
-rw-r--r--spec/lib/google_api/auth_spec.rb41
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb128
-rw-r--r--spec/lib/rspec_flaky/config_spec.rb102
-rw-r--r--spec/lib/rspec_flaky/flaky_example_spec.rb129
-rw-r--r--spec/lib/rspec_flaky/flaky_examples_collection_spec.rb79
-rw-r--r--spec/lib/rspec_flaky/listener_spec.rb173
-rw-r--r--spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb8
-rw-r--r--spec/mailers/emails/profile_spec.rb25
-rw-r--r--spec/mailers/notify_spec.rb491
-rw-r--r--spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb5
-rw-r--r--spec/migrations/normalize_ldap_extern_uids_spec.rb56
-rw-r--r--spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb43
-rw-r--r--spec/migrations/update_upload_paths_to_system_spec.rb4
-rw-r--r--spec/models/ci/artifact_blob_spec.rb50
-rw-r--r--spec/models/ci/build_spec.rb12
-rw-r--r--spec/models/ci/build_trace_section_name_spec.rb12
-rw-r--r--spec/models/ci/build_trace_section_spec.rb11
-rw-r--r--spec/models/ci/pipeline_spec.rb10
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb72
-rw-r--r--spec/models/concerns/has_status_spec.rb12
-rw-r--r--spec/models/concerns/issuable_spec.rb80
-rw-r--r--spec/models/concerns/routable_spec.rb10
-rw-r--r--spec/models/diff_note_spec.rb40
-rw-r--r--spec/models/email_spec.rb29
-rw-r--r--spec/models/fork_network_member_spec.rb8
-rw-r--r--spec/models/fork_network_spec.rb54
-rw-r--r--spec/models/forked_project_link_spec.rb12
-rw-r--r--spec/models/gcp/cluster_spec.rb240
-rw-r--r--spec/models/gpg_key_spec.rb42
-rw-r--r--spec/models/gpg_key_subkey_spec.rb15
-rw-r--r--spec/models/gpg_signature_spec.rb52
-rw-r--r--spec/models/issue_spec.rb16
-rw-r--r--spec/models/member_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb80
-rw-r--r--spec/models/namespace_spec.rb51
-rw-r--r--spec/models/note_spec.rb50
-rw-r--r--spec/models/project_group_link_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/issue_message_spec.rb12
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb12
-rw-r--r--spec/models/project_services/chat_message/note_message_spec.rb22
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb15
-rw-r--r--spec/models/project_services/chat_message/wiki_page_message_spec.rb12
-rw-r--r--spec/models/project_spec.rb113
-rw-r--r--spec/models/repository_spec.rb36
-rw-r--r--spec/models/sent_notification_spec.rb122
-rw-r--r--spec/models/user_spec.rb230
-rw-r--r--spec/policies/gcp/cluster_policy_spec.rb28
-rw-r--r--spec/policies/issuable_policy_spec.rb28
-rw-r--r--spec/policies/note_policy_spec.rb71
-rw-r--r--spec/presenters/ci/pipeline_presenter_spec.rb17
-rw-r--r--spec/presenters/gcp/cluster_presenter_spec.rb35
-rw-r--r--spec/requests/api/helpers_spec.rb28
-rw-r--r--spec/requests/api/merge_requests_spec.rb40
-rw-r--r--spec/requests/api/notes_spec.rb34
-rw-r--r--spec/requests/api/projects_spec.rb5
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb40
-rw-r--r--spec/requests/lfs_http_spec.rb34
-rw-r--r--spec/rubocop/cop/migration/datetime_spec.rb26
-rw-r--r--spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb53
-rw-r--r--spec/serializers/build_details_entity_spec.rb10
-rw-r--r--spec/serializers/cluster_entity_spec.rb22
-rw-r--r--spec/serializers/cluster_serializer_spec.rb19
-rw-r--r--spec/serializers/merge_request_entity_spec.rb2
-rw-r--r--spec/serializers/pipeline_entity_spec.rb13
-rw-r--r--spec/services/ci/create_cluster_service_spec.rb47
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb8
-rw-r--r--spec/services/ci/extract_sections_from_build_trace_service_spec.rb55
-rw-r--r--spec/services/ci/fetch_gcp_operation_service_spec.rb36
-rw-r--r--spec/services/ci/fetch_kubernetes_token_service_spec.rb64
-rw-r--r--spec/services/ci/finalize_cluster_creation_service_spec.rb61
-rw-r--r--spec/services/ci/integrate_cluster_service_spec.rb42
-rw-r--r--spec/services/ci/provision_cluster_service_spec.rb85
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/ci/update_cluster_service_spec.rb37
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb6
-rw-r--r--spec/services/discussions/update_diff_position_service_spec.rb6
-rw-r--r--spec/services/emails/confirm_service_spec.rb15
-rw-r--r--spec/services/emails/create_service_spec.rb5
-rw-r--r--spec/services/emails/destroy_service_spec.rb4
-rw-r--r--spec/services/gpg_keys/create_service_spec.rb10
-rw-r--r--spec/services/issues/update_service_spec.rb36
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb20
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb4
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb16
-rw-r--r--spec/services/merge_requests/update_service_spec.rb16
-rw-r--r--spec/services/notification_service_spec.rb12
-rw-r--r--spec/services/projects/destroy_service_spec.rb13
-rw-r--r--spec/services/projects/fork_service_spec.rb39
-rw-r--r--spec/services/projects/hashed_storage_migration_service_spec.rb2
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb30
-rw-r--r--spec/services/projects/update_service_spec.rb10
-rw-r--r--spec/services/system_note_service_spec.rb38
-rw-r--r--spec/services/users/activity_service_spec.rb12
-rw-r--r--spec/spec_helper.rb5
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb2
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb6
-rw-r--r--spec/support/gpg_helpers.rb313
-rw-r--r--spec/support/helpers/merge_request_diff_helpers.rb28
-rw-r--r--spec/support/ldap_shared_examples.rb69
-rw-r--r--spec/support/project_forks_helper.rb58
-rw-r--r--spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb57
-rw-r--r--spec/support/shared_examples/models/project_hook_data_shared_examples.rb (renamed from spec/support/project_hook_data_shared_example.rb)6
-rw-r--r--spec/support/shared_examples/position_formatters.rb43
-rw-r--r--spec/support/test_env.rb3
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb17
-rw-r--r--spec/views/projects/merge_requests/_commits.html.haml_spec.rb5
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb9
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb11
-rw-r--r--spec/workers/build_finished_worker_spec.rb2
-rw-r--r--spec/workers/build_trace_sections_worker_spec.rb23
-rw-r--r--spec/workers/cluster_provision_worker_spec.rb23
-rw-r--r--spec/workers/concerns/cluster_queue_spec.rb15
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb2
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb10
-rw-r--r--spec/workers/repository_fork_worker_spec.rb22
-rw-r--r--spec/workers/repository_import_worker_spec.rb17
-rw-r--r--spec/workers/wait_for_cluster_creation_worker_spec.rb67
-rw-r--r--yarn.lock4
1038 files changed, 21076 insertions, 5839 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8501911fde4..fc0d2b71174 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -27,7 +27,7 @@ variables:
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
- FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/${CI_PROJECT_NAME}/report-master.json
+ FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json
before_script:
- bundle --version
@@ -87,12 +87,13 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[-1]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- - export ALL_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/all_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- - export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/new_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export SUITE_FLAKY_RSPEC_REPORT_PATH=${FLAKY_RSPEC_SUITE_REPORT_PATH}
+ - export FLAKY_RSPEC_REPORT_PATH=rspec_flaky/all_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/new_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export FLAKY_RSPEC_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- - cp ${FLAKY_RSPEC_SUITE_REPORT_PATH} ${ALL_FLAKY_RSPEC_REPORT_PATH}
+ - '[[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}'
- '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}'
- scripts/gitaly-test-spawn
- knapsack rspec "--color --format documentation"
@@ -233,7 +234,7 @@ retrieve-tests-metadata:
- wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
- - mkdir -p rspec_flaky/${CI_PROJECT_NAME}/
+ - mkdir -p rspec_flaky/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
@@ -252,24 +253,24 @@ update-tests-metadata:
- retry gem install fog-aws mime-types
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json
+ - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- - rm -f rspec_flaky/${CI_PROJECT_NAME}/*_node_*.json
+ - rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
flaky-examples-check:
<<: *dedicated-runner
image: ruby:2.3-alpine
services: []
before_script: []
- cache: {}
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
- NEW_FLAKY_SPECS_REPORT: rspec_flaky/${CI_PROJECT_NAME}/new_rspec_flaky_examples.json
+ NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json
stage: post-test
allow_failure: yes
+ retry: 0
only:
- branches
except:
@@ -281,7 +282,7 @@ flaky-examples-check:
- rspec_flaky/
script:
- '[[ -f $NEW_FLAKY_SPECS_REPORT ]] || echo "{}" > ${NEW_FLAKY_SPECS_REPORT}'
- - scripts/merge-reports $NEW_FLAKY_SPECS_REPORT rspec_flaky/${CI_PROJECT_NAME}/new_node_*.json
+ - scripts/merge-reports ${NEW_FLAKY_SPECS_REPORT} rspec_flaky/new_*_*.json
- scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
setup-test-env:
@@ -429,6 +430,7 @@ ee_compat_check:
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
allow_failure: yes
+ retry: 0
cache:
key: "ee_compat_check_repo"
paths:
@@ -579,7 +581,7 @@ karma:
- chrome_debug.log
- coverage-javascript/
-codeclimate:
+codequality:
<<: *except-docs
<<: *pull-cache
before_script: []
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 15d9117976a..efd32d44890 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.0.3 (2017-10-05)
+
+- [FIXED] find_user Users helper method no longer overrides find_user API helper method. !14418
+- [FIXED] Fix CSRF validation issue when closing/opening merge requests from the UI. !14555
+- [FIXED] Kubernetes integration: ensure v1.8.0 compatibility. !14635
+- [FIXED] Fixes data parameter not being sent in ajax request for jobs log.
+- [FIXED] Improves UX of autodevops popover to match gpg one.
+- [FIXED] Fixed commenting on side-by-side commit diff.
+- [FIXED] Make sure API responds with 401 when invalid authentication info is provided.
+- [FIXED] Fix merge request counter updates after merge.
+- [FIXED] Fix gitlab-rake gitlab:import:repos task failing.
+- [FIXED] Fix pushes to an empty repository not invalidating has_visible_content? cache.
+- [FIXED] Ensure all refs are restored on a restore from backup.
+- [FIXED] Gitaly RepositoryExists remains opt-in for all method calls.
+- [FIXED] Fix 500 error on merged merge requests when GitLab is restored from a backup.
+- [FIXED] Adjust MRs being stuck on "process of being merged" for more than 2 hours.
+
## 10.0.2 (2017-09-27)
- [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450
@@ -195,6 +212,10 @@ entry.
- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi)
- [BUGIFX] Improves subgroup creation permissions. !13418
+## 9.5.8 (2017-10-04)
+
+- [FIXED] Fixed fork button being disabled for users who can fork to a group.
+
## 9.5.7 (2017-10-03)
- Fix gitlab rake:import:repos task.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 8298bb08b2d..ff12df2fbb0 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.43.0
+0.45.1
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index fd2a01863fd..944880fa15e 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-3.1.0
+3.2.0
diff --git a/Gemfile b/Gemfile
index 44c459c497f..c1988aea803 100644
--- a/Gemfile
+++ b/Gemfile
@@ -23,7 +23,7 @@ gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
-gem 'doorkeeper-openid_connect', '~> 1.1.0'
+gem 'doorkeeper-openid_connect', '~> 1.2.0'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.9'
@@ -398,7 +398,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.39.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.41.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 4622fd141ba..f5712da7508 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -80,7 +80,7 @@ GEM
coderay (>= 1.0.0)
erubis (>= 2.6.6)
rack (>= 0.9.0)
- bindata (2.3.5)
+ bindata (2.4.1)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
@@ -166,9 +166,9 @@ GEM
docile (1.1.5)
domain_name (0.5.20161021)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (4.2.0)
+ doorkeeper (4.2.6)
railties (>= 4.2)
- doorkeeper-openid_connect (1.1.2)
+ doorkeeper-openid_connect (1.2.0)
doorkeeper (~> 4.0)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
@@ -273,7 +273,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.39.0)
+ gitaly-proto (0.41.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -410,7 +410,7 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.6)
- json-jwt (1.7.1)
+ json-jwt (1.7.2)
activesupport
bindata
multi_json (>= 1.3)
@@ -681,7 +681,7 @@ GEM
rainbow (2.2.2)
rake
raindrops (0.18.0)
- rake (12.0.0)
+ rake (12.1.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rbnacl (4.0.2)
@@ -1002,7 +1002,7 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
- doorkeeper-openid_connect (~> 1.1.0)
+ doorkeeper-openid_connect (~> 1.2.0)
dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
@@ -1027,7 +1027,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.39.0)
+ gitaly-proto (~> 0.41.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
diff --git a/app/assets/images/auth_buttons/signin_with_google.png b/app/assets/images/auth_buttons/signin_with_google.png
new file mode 100644
index 00000000000..b1327b4f7b4
--- /dev/null
+++ b/app/assets/images/auth_buttons/signin_with_google.png
Binary files differ
diff --git a/app/assets/images/icon_image_comment.svg b/app/assets/images/icon_image_comment.svg
new file mode 100644
index 00000000000..cf6cb972940
--- /dev/null
+++ b/app/assets/images/icon_image_comment.svg
@@ -0,0 +1 @@
+<svg width="24" height="30" viewBox="0 0 24 30" xmlns="http://www.w3.org/2000/svg"><title>cursor</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#1F78D1" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#FFF"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787c-.91 0-1.763.156-2.558.469-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068 0-.009.01-.031.033-.067a.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26.094-.126.168-.24.221-.342.054-.103.114-.235.181-.395.067-.161.125-.33.174-.51-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
diff --git a/app/assets/images/icon_image_comment@2x.svg b/app/assets/images/icon_image_comment@2x.svg
new file mode 100644
index 00000000000..83be91d3705
--- /dev/null
+++ b/app/assets/images/icon_image_comment@2x.svg
@@ -0,0 +1 @@
+<svg width="48" height="60" viewBox="0 0 48 60" xmlns="http://www.w3.org/2000/svg"><title>cursor_2x</title><g fill="none" fill-rule="evenodd"><path d="M48 24.21C48 37.583 36.522 47.369 24 60 11.478 47.368 0 37.582 0 24.21 0 10.84 10.745 0 24 0s24 10.84 24 24.21z" fill="#1F78D1" fill-rule="nonzero"/><path d="M30.56 50.497c2.915-2.95 5.078-5.268 6.947-7.493 5.703-6.788 8.406-12.53 8.406-18.793 0-12.223-9.815-22.124-21.913-22.124S2.087 11.988 2.087 24.211c0 6.263 2.703 12.005 8.406 18.793 1.87 2.225 4.032 4.544 6.947 7.493 1.022 1.035 4.432 4.426 6.56 6.55 2.128-2.124 5.538-5.515 6.56-6.55z" fill="#FFF"/><path d="M29.103 16.512c-1.58-.625-3.282-.938-5.103-.938-1.821 0-3.527.313-5.116.938-1.58.616-2.84 1.45-3.777 2.504-.928 1.054-1.393 2.192-1.393 3.415 0 1 .317 1.956.951 2.866.643.902 1.545 1.684 2.706 2.344l1.165.67-.362 1.286a9.603 9.603 0 0 1-.937 2.303 13.208 13.208 0 0 0 3.683-2.29l.576-.509.763.08c.616.072 1.196.108 1.741.108 1.821 0 3.522-.308 5.103-.925 1.589-.625 2.848-1.464 3.776-2.517.938-1.054 1.407-2.192 1.407-3.416 0-1.223-.469-2.361-1.407-3.415-.928-1.053-2.187-1.888-3.776-2.504zm5.29 1.62c1.071 1.313 1.607 2.746 1.607 4.3 0 1.553-.536 2.99-1.607 4.312-1.072 1.312-2.527 2.353-4.366 3.12-1.84.76-3.848 1.139-6.027 1.139a18.32 18.32 0 0 1-1.942-.107c-1.768 1.562-3.821 2.643-6.16 3.24-.438.126-.947.224-1.527.295h-.067a.521.521 0 0 1-.362-.147.649.649 0 0 1-.214-.362v-.013c-.027-.036-.032-.09-.014-.16.027-.072.036-.117.027-.135 0-.017.022-.062.067-.133a1.29 1.29 0 0 0 .08-.121c.01-.009.04-.045.094-.107a106.068 106.068 0 0 1 .522-.59c.215-.232.367-.401.456-.508.098-.099.236-.273.415-.523.188-.25.335-.477.442-.683.107-.205.228-.468.362-.79.134-.321.25-.66.348-1.018-1.402-.794-2.51-1.777-3.322-2.946C12.402 25.025 12 23.77 12 22.43c0-1.553.536-2.986 1.607-4.299 1.072-1.321 2.527-2.361 4.366-3.12 1.84-.768 3.848-1.152 6.027-1.152 2.179 0 4.188.384 6.027 1.152 1.84.759 3.294 1.799 4.366 3.12z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json
index 6b8f85e37fd..c0ed2ffdcb2 100644
--- a/app/assets/images/icons.json
+++ b/app/assets/images/icons.json
@@ -1 +1 @@
-{"iconCount":135,"spriteSize":58718,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","comment-dots","comment-next","comment","comments","commit","credit-card","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file
+{"iconCount":164,"spriteSize":72823,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","dashboard","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg
index 30cb2109ec2..b9829d0d450 100644
--- a/app/assets/images/icons.svg
+++ b/app/assets/images/icons.svg
@@ -1 +1 @@
-<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.535 7.95L1.292 3.707a1 1 0 0 1-.281-.695 1 1 0 1 1 1.695-.719l4.95 4.95a.998.998 0 0 1 0 1.414l-4.95 4.95a1.002 1.002 0 0 1-.707.293c-.549 0-1-.452-1-1 0-.266.106-.52.293-.708L5.535 7.95zm7 0L8.292 3.707a1 1 0 0 1-.281-.695 1 1 0 1 1 1.695-.719l4.95 4.95a.998.998 0 0 1 0 1.414l-4.95 4.95a1.002 1.002 0 0 1-.707.293c-.549 0-1-.452-1-1.001 0-.265.106-.519.293-.707l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 15V1a1 1 0 0 1 1-1h4.604c.93 0 1.762.088 2.495.264.733.176 1.353.445 1.863.807.509.363.897.82 1.164 1.369.268.549.401 1.197.401 1.945 0 .366-.045.718-.137 1.055-.091.337-.23.652-.417.945a3.453 3.453 0 0 1-.71.796 3.645 3.645 0 0 1-1.021.588c.469.117.87.295 1.203.533.333.238.608.515.824.83.216.315.374.657.473 1.027.099.37.148.75.148 1.138 0 1.553-.5 2.725-1.5 3.516-1 .791-2.423 1.187-4.27 1.187H3a1 1 0 0 1-1-1zm3.297-5.967v4.319H8.12c.425 0 .791-.053 1.099-.16.307-.106.564-.252.769-.44.205-.186.357-.406.456-.659.099-.252.148-.529.148-.83a3.04 3.04 0 0 0-.131-.928 1.78 1.78 0 0 0-.413-.703 1.8 1.8 0 0 0-.73-.445c-.3-.103-.66-.154-1.077-.154H5.297zm0-2.33h2.44c.842-.014 1.468-.192 1.878-.533.41-.34.616-.826.616-1.456 0-.725-.21-1.247-.632-1.566-.421-.318-1.086-.478-1.995-.478H5.297v4.033z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-additions" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.009 6.958a4 4 0 0 0 5.283 4.775 1 1 0 0 1 .712 1.87A6 6 0 0 1 2.077 6.44l-.741-.2a.5.5 0 0 1-.12-.915L3.41 4.058a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.711-1.87 6 6 0 0 1 7.927 7.162l.74.2a.5.5 0 0 1 .121.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="talic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 0h7a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm2 2h3L8 14H5L8 2zM3 14h7a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 15V1a1 1 0 0 1 1-1h4.604c.93 0 1.762.088 2.495.264.733.176 1.353.445 1.863.807.509.363.897.82 1.164 1.369.268.549.401 1.197.401 1.945 0 .366-.045.718-.137 1.055-.091.337-.23.652-.417.945a3.453 3.453 0 0 1-.71.796 3.645 3.645 0 0 1-1.021.588c.469.117.87.295 1.203.533.333.238.608.515.824.83.216.315.374.657.473 1.027.099.37.148.75.148 1.138 0 1.553-.5 2.725-1.5 3.516-1 .791-2.423 1.187-4.27 1.187H3a1 1 0 0 1-1-1zm3.297-5.967v4.319H8.12c.425 0 .791-.053 1.099-.16.307-.106.564-.252.769-.44.205-.186.357-.406.456-.659.099-.252.148-.529.148-.83a3.04 3.04 0 0 0-.131-.928 1.78 1.78 0 0 0-.413-.703 1.8 1.8 0 0 0-.73-.445c-.3-.103-.66-.154-1.077-.154H5.297zm0-2.33h2.44c.842-.014 1.468-.192 1.878-.533.41-.34.616-.826.616-1.456 0-.725-.21-1.247-.632-1.566-.421-.318-1.086-.478-1.995-.478H5.297v4.033z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-additions" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 24 30" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><title>cursor_active</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#FFF" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#1F78D1"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787a6.92 6.92 0 0 0-2.558.469c-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068a.19.19 0 0 1 .033-.067.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26a2.57 2.57 0 0 0 .221-.342c.054-.103.114-.235.181-.395a4.18 4.18 0 0 0 .174-.51c-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#FFF" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.009 6.958a4 4 0 0 0 5.283 4.775 1 1 0 0 1 .712 1.87A6 6 0 0 1 2.077 6.44l-.741-.2a.5.5 0 0 1-.12-.915L3.41 4.058a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.711-1.87 6 6 0 0 1 7.927 7.162l.74.2a.5.5 0 0 1 .121.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="ehfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="ehsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="ehthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="talic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 0h7a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm2 2h3L8 14H5L8 2zM3 14h7a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
diff --git a/app/assets/images/new_nav.png b/app/assets/images/new_nav.png
deleted file mode 100644
index f98ca15d787..00000000000
--- a/app/assets/images/new_nav.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/old_nav.png b/app/assets/images/old_nav.png
deleted file mode 100644
index 23fae7aa19e..00000000000
--- a/app/assets/images/old_nav.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js
index 346de4ad11e..3de192d56eb 100644
--- a/app/assets/javascripts/abuse_reports.js
+++ b/app/assets/javascripts/abuse_reports.js
@@ -1,7 +1,7 @@
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
-class AbuseReports {
+export default class AbuseReports {
constructor() {
$(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
$(document)
@@ -32,6 +32,3 @@ class AbuseReports {
}
}
}
-
-window.gl = window.gl || {};
-window.gl.AbuseReports = AbuseReports;
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js
index 8f5e2e545ec..2bc77859c26 100644
--- a/app/assets/javascripts/ajax_loading_spinner.js
+++ b/app/assets/javascripts/ajax_loading_spinner.js
@@ -1,4 +1,4 @@
-class AjaxLoadingSpinner {
+export default class AjaxLoadingSpinner {
static init() {
const $elements = $('.js-ajax-loading-spinner');
@@ -30,6 +30,3 @@ class AjaxLoadingSpinner {
classList.toggle('fa-spin');
}
}
-
-window.gl = window.gl || {};
-gl.AjaxLoadingSpinner = AjaxLoadingSpinner;
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 4f01345ee3b..622764107ad 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,8 +1,8 @@
/* eslint-disable class-methods-use-this */
-/* global Flash */
import _ from 'underscore';
import Cookies from 'js-cookie';
import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
+import Flash from './flash';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index 8641a6fdae6..062577af385 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -1,9 +1,8 @@
-/* global Flash */
-
+import Flash from '../flash';
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
function onError() {
- const flash = new window.Flash('Balsamiq file could not be loaded.');
+ const flash = new Flash('Balsamiq file could not be loaded.');
return flash;
}
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index a20c6ca7a21..583e5faa506 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -1,6 +1,5 @@
/* eslint-disable class-methods-use-this */
-/* global Flash */
-
+import Flash from '../flash';
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index e0b73f13d36..54132e8537b 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,4 +1,4 @@
-/* global Flash */
+import Flash from '../../flash';
import { handleLocationHash } from '../../lib/utils/common_utils';
export default class BlobViewer {
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index 815248f38ee..ef4093b59e3 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -1,10 +1,10 @@
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */
-/* global Flash */
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
+import Flash from '../flash';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
import './models/issue';
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 590b7be36e3..7f3afefc9cc 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -3,9 +3,9 @@
/* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index a656f0546c0..de9e44cef35 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../../../flash';
import './lists_dropdown';
const ModalStore = gl.issueBoards.ModalStore;
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index d7f203b3f96..c19c989680d 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -1,6 +1,7 @@
-/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var,
+/* eslint-disable func-names, no-new, space-before-function-paren, one-var,
promise/catch-or-return */
import _ from 'underscore';
+import CreateLabelDropdown from '../../create_label';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -15,15 +16,15 @@ $(document).off('created.label').on('created.label', (e, label) => {
label: {
id: label.id,
title: label.title,
- color: label.color
- }
+ color: label.color,
+ },
});
});
gl.issueBoards.newListDropdownInit = () => {
$('.js-new-board-list').each(function () {
const $this = $(this);
- new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
+ new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({
data(term, callback) {
@@ -38,17 +39,17 @@ gl.issueBoards.newListDropdownInit = () => {
const $a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''),
text: label.title,
- href: '#'
+ href: '#',
});
const $labelColor = $('<span />', {
class: 'dropdown-label-box',
- style: `background-color: ${label.color}`
+ style: `background-color: ${label.color}`,
});
return $li.append($a.prepend($labelColor));
},
search: {
- fields: ['title']
+ fields: ['title'],
},
filterable: true,
selectable: true,
@@ -66,13 +67,13 @@ gl.issueBoards.newListDropdownInit = () => {
label: {
id: label.id,
title: label.title,
- color: label.color
- }
+ color: label.color,
+ },
});
Store.state.lists = _.sortBy(Store.state.lists, 'position');
}
- }
+ },
});
});
};
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index 1e623cf58b7..1ad97211934 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../../../flash';
const Store = gl.issueBoards.BoardsStore;
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index bd479700fd3..ace89398943 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,25 +1,45 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
+/* eslint-disable func-names, prefer-arrow-callback, no-return-assign */
+import { visitUrl } from './lib/utils/url_utility';
+import { convertPermissionToBoolean } from './lib/utils/common_utils';
-window.BuildArtifacts = (function() {
- function BuildArtifacts() {
+export default class BuildArtifacts {
+ constructor() {
this.disablePropagation();
this.setupEntryClick();
+ this.setupTooltips();
}
-
- BuildArtifacts.prototype.disablePropagation = function() {
- $('.top-block').on('click', '.download', function(e) {
+ // eslint-disable-next-line class-methods-use-this
+ disablePropagation() {
+ $('.top-block').on('click', '.download', function (e) {
return e.stopPropagation();
});
- return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
+ return $('.tree-holder').on('click', 'tr[data-link] a', function (e) {
return e.stopImmediatePropagation();
});
- };
-
- BuildArtifacts.prototype.setupEntryClick = function() {
- return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
- return window.location = this.dataset.link;
+ }
+ // eslint-disable-next-line class-methods-use-this
+ setupEntryClick() {
+ return $('.tree-holder').on('click', 'tr[data-link]', function () {
+ visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));
+ });
+ }
+ // eslint-disable-next-line class-methods-use-this
+ setupTooltips() {
+ $('.js-artifact-tree-tooltip').tooltip({
+ placement: 'bottom',
+ // Stop the tooltip from hiding when we stop hovering the element directly
+ // We handle all the showing/hiding below
+ trigger: 'manual',
});
- };
- return BuildArtifacts;
-})();
+ // We want the tooltip to show if you hover anywhere on the row
+ // But be placed below and in the middle of the file name
+ $('.js-artifact-tree-row')
+ .on('mouseenter', (e) => {
+ $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show');
+ })
+ .on('mouseleave', (e) => {
+ $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide');
+ });
+ }
+}
diff --git a/app/assets/javascripts/build_variables.js b/app/assets/javascripts/build_variables.js
index c955a9ac2ea..35edf3e0017 100644
--- a/app/assets/javascripts/build_variables.js
+++ b/app/assets/javascripts/build_variables.js
@@ -1,8 +1,10 @@
-/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren */
+/* eslint-disable func-names*/
-$(function() {
- $('.reveal-variables').off('click').on('click', function() {
- $('.js-build-variables').toggle();
- $(this).hide();
- });
-});
+export default function handleRevealVariables() {
+ $('.js-reveal-variables')
+ .off('click')
+ .on('click', function () {
+ $('.js-build-variables').toggle();
+ $(this).hide();
+ });
+}
diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/ci_lint_editor.js
index dd4a08a2f31..b9469e5b7cb 100644
--- a/app/assets/javascripts/ci_lint_editor.js
+++ b/app/assets/javascripts/ci_lint_editor.js
@@ -1,7 +1,4 @@
-
-window.gl = window.gl || {};
-
-class CILintEditor {
+export default class CILintEditor {
constructor() {
this.editor = window.ace.edit('ci-editor');
this.textarea = document.querySelector('#content');
@@ -13,5 +10,3 @@ class CILintEditor {
});
}
}
-
-gl.CILintEditor = CILintEditor;
diff --git a/app/assets/javascripts/clusters.js b/app/assets/javascripts/clusters.js
new file mode 100644
index 00000000000..50dbeb06362
--- /dev/null
+++ b/app/assets/javascripts/clusters.js
@@ -0,0 +1,112 @@
+/* globals Flash */
+import Visibility from 'visibilityjs';
+import axios from 'axios';
+import Poll from './lib/utils/poll';
+import { s__ } from './locale';
+import './flash';
+
+/**
+ * Cluster page has 2 separate parts:
+ * Toggle button
+ *
+ * - Polling status while creating or scheduled
+ * -- Update status area with the response result
+ */
+
+class ClusterService {
+ constructor(options = {}) {
+ this.options = options;
+ }
+ fetchData() {
+ return axios.get(this.options.endpoint);
+ }
+}
+
+export default class Clusters {
+ constructor() {
+ const dataset = document.querySelector('.js-edit-cluster-form').dataset;
+
+ this.state = {
+ statusPath: dataset.statusPath,
+ clusterStatus: dataset.clusterStatus,
+ clusterStatusReason: dataset.clusterStatusReason,
+ toggleStatus: dataset.toggleStatus,
+ };
+
+ this.service = new ClusterService({ endpoint: this.state.statusPath });
+ this.toggleButton = document.querySelector('.js-toggle-cluster');
+ this.toggleInput = document.querySelector('.js-toggle-input');
+ this.errorContainer = document.querySelector('.js-cluster-error');
+ this.successContainer = document.querySelector('.js-cluster-success');
+ this.creatingContainer = document.querySelector('.js-cluster-creating');
+ this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
+
+ this.toggleButton.addEventListener('click', this.toggle.bind(this));
+
+ if (this.state.clusterStatus !== 'created') {
+ this.updateContainer(this.state.clusterStatus, this.state.clusterStatusReason);
+ }
+
+ if (this.state.statusPath) {
+ this.initPolling();
+ }
+ }
+
+ toggle() {
+ this.toggleButton.classList.toggle('checked');
+ this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
+ }
+
+ initPolling() {
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'fetchData',
+ successCallback: (data) => {
+ const { status, status_reason } = data.data;
+ this.updateContainer(status, status_reason);
+ },
+ errorCallback: () => {
+ Flash(s__('ClusterIntegration|Something went wrong on our end.'));
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ this.poll.makeRequest();
+ } else {
+ this.service.fetchData();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ }
+
+ hideAll() {
+ this.errorContainer.classList.add('hidden');
+ this.successContainer.classList.add('hidden');
+ this.creatingContainer.classList.add('hidden');
+ }
+
+ updateContainer(status, error) {
+ this.hideAll();
+ switch (status) {
+ case 'created':
+ this.successContainer.classList.remove('hidden');
+ break;
+ case 'errored':
+ this.errorContainer.classList.remove('hidden');
+ this.errorReasonContainer.textContent = error;
+ break;
+ case 'scheduled':
+ case 'creating':
+ this.creatingContainer.classList.remove('hidden');
+ break;
+ default:
+ this.hideAll();
+ }
+ }
+}
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
deleted file mode 100644
index 5f637524e30..00000000000
--- a/app/assets/javascripts/commit.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife */
-/* global CommitFile */
-
-window.Commit = (function() {
- function Commit() {
- $('.files .diff-file').each(function() {
- return new CommitFile(this);
- });
- }
-
- return Commit;
-})();
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
deleted file mode 100644
index ee087c978dd..00000000000
--- a/app/assets/javascripts/commit/file.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */
-/* global ImageFile */
-
-(function() {
- this.CommitFile = (function() {
- function CommitFile(file) {
- if ($('.image', file).length) {
- new gl.ImageFile(file);
- }
- }
-
- return CommitFile;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 4763985c802..e7adf8814b8 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,4 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
+import 'vendor/jquery.waitforimages';
+
(function() {
gl.ImageFile = (function() {
var prepareFrames;
@@ -17,15 +19,10 @@
// Load two-up view after images are loaded
// so that we can display the correct width and height information
- const images = $('.two-up.view img', _this.file);
- let loadedCount = 0;
-
- images.on('load', () => {
- loadedCount += 1;
+ const $images = $('.two-up.view img', _this.file);
- if (loadedCount === images.length) {
- _this.initView('two-up');
- }
+ $images.waitForImages(function() {
+ _this.initView('two-up');
});
});
};
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 047544b1762..ae6b8902032 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,17 +1,19 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */
+/* eslint-disable func-names, wrap-iife, consistent-return,
+ no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars,
+ prefer-template, object-shorthand, prefer-arrow-callback */
/* global Pager */
-window.CommitsList = (function() {
- var CommitsList = {};
+export default (function () {
+ const CommitsList = {};
CommitsList.timer = null;
- CommitsList.init = function(limit) {
+ CommitsList.init = function (limit) {
this.$contentList = $('.content_list');
- $("body").on("click", ".day-commits-table li.commit", function(e) {
- if (e.target.nodeName !== "A") {
- location.href = $(this).attr("url");
+ $('body').on('click', '.day-commits-table li.commit', function (e) {
+ if (e.target.nodeName !== 'A') {
+ location.href = $(this).attr('url');
e.stopPropagation();
return false;
}
@@ -19,48 +21,47 @@ window.CommitsList = (function() {
Pager.init(parseInt(limit, 10), false, false, this.processCommits);
- this.content = $("#commits-list");
- this.searchField = $("#commits-search");
+ this.content = $('#commits-list');
+ this.searchField = $('#commits-search');
this.lastSearch = this.searchField.val();
return this.initSearch();
};
- CommitsList.initSearch = function() {
+ CommitsList.initSearch = function () {
this.timer = null;
- return this.searchField.keyup((function(_this) {
- return function() {
+ return this.searchField.keyup((function (_this) {
+ return function () {
clearTimeout(_this.timer);
return _this.timer = setTimeout(_this.filterResults, 500);
};
})(this));
};
- CommitsList.filterResults = function() {
- var commitsUrl, form, search;
- form = $(".commits-search-form");
- search = CommitsList.searchField.val();
+ CommitsList.filterResults = function () {
+ const form = $('.commits-search-form');
+ const search = CommitsList.searchField.val();
if (search === CommitsList.lastSearch) return;
- commitsUrl = form.attr("action") + '?' + form.serialize();
+ const commitsUrl = form.attr('action') + '?' + form.serialize();
CommitsList.content.fadeTo('fast', 0.5);
return $.ajax({
- type: "GET",
- url: form.attr("action"),
+ type: 'GET',
+ url: form.attr('action'),
data: form.serialize(),
- complete: function() {
+ complete: function () {
return CommitsList.content.fadeTo('fast', 1.0);
},
- success: function(data) {
+ success: function (data) {
CommitsList.lastSearch = search;
CommitsList.content.html(data.html);
return history.replaceState({
- page: commitsUrl
+ page: commitsUrl,
// Change url so if user reload a page - search results are saved
}, document.title, commitsUrl);
},
- error: function() {
+ error: function () {
CommitsList.lastSearch = null;
},
- dataType: "json"
+ dataType: 'json',
});
};
@@ -81,7 +82,7 @@ window.CommitsList = (function() {
commitsCount = $commitsHeadersLast.nextUntil('li.js-commit-header').find('li.commit').length;
// Remove duplicate of commits header.
- processedData = $processedData.not(`li.js-commit-header[data-day="${loadedShownDayFirst}"]`);
+ processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`);
// Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js
index 907b468e576..3bed0678350 100644
--- a/app/assets/javascripts/create_label.js
+++ b/app/assets/javascripts/create_label.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */
+/* eslint-disable func-names, prefer-arrow-callback */
import Api from './api';
-class CreateLabelDropdown {
- constructor ($el, namespacePath, projectPath) {
+export default class CreateLabelDropdown {
+ constructor($el, namespacePath, projectPath) {
this.$el = $el;
this.namespacePath = namespacePath;
this.projectPath = projectPath;
@@ -22,7 +22,7 @@ class CreateLabelDropdown {
this.addBinding();
}
- cleanBinding () {
+ cleanBinding() {
this.$colorSuggestions.off('click');
this.$newLabelField.off('keyup change');
this.$newColorField.off('keyup change');
@@ -31,7 +31,7 @@ class CreateLabelDropdown {
this.$newLabelCreateButton.off('click');
}
- addBinding () {
+ addBinding() {
const self = this;
this.$colorSuggestions.on('click', function (e) {
@@ -44,7 +44,7 @@ class CreateLabelDropdown {
this.$dropdownBack.on('click', this.resetForm.bind(this));
- this.$cancelButton.on('click', function(e) {
+ this.$cancelButton.on('click', function (e) {
e.preventDefault();
e.stopPropagation();
@@ -55,7 +55,7 @@ class CreateLabelDropdown {
this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
}
- addColorValue (e, $this) {
+ addColorValue(e, $this) {
e.preventDefault();
e.stopPropagation();
@@ -66,7 +66,7 @@ class CreateLabelDropdown {
.addClass('is-active');
}
- enableLabelCreateButton () {
+ enableLabelCreateButton() {
if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
this.$newLabelError.hide();
this.$newLabelCreateButton.enable();
@@ -75,7 +75,7 @@ class CreateLabelDropdown {
}
}
- resetForm () {
+ resetForm() {
this.$newLabelField
.val('')
.trigger('change');
@@ -90,13 +90,13 @@ class CreateLabelDropdown {
.removeClass('is-active');
}
- saveLabel (e) {
+ saveLabel(e) {
e.preventDefault();
e.stopPropagation();
Api.newLabel(this.namespacePath, this.projectPath, {
title: this.$newLabelField.val(),
- color: this.$newColorField.val()
+ color: this.$newColorField.val(),
}, (label) => {
this.$newLabelCreateButton.enable();
@@ -107,8 +107,8 @@ class CreateLabelDropdown {
errors = label.message;
} else {
errors = Object.keys(label.message).map(key =>
- `${gl.text.humanize(key)} ${label.message[key].join(', ')}`
- ).join("<br/>");
+ `${gl.text.humanize(key)} ${label.message[key].join(', ')}`,
+ ).join('<br/>');
}
this.$newLabelError
@@ -122,6 +122,3 @@ class CreateLabelDropdown {
});
}
}
-
-window.gl = window.gl || {};
-gl.CreateLabelDropdown = CreateLabelDropdown;
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index ff2f2c81971..bf40eb3ee11 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -1,5 +1,5 @@
/* eslint-disable no-new */
-/* global Flash */
+import Flash from './flash';
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue
new file mode 100644
index 00000000000..732697c134e
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/banner.vue
@@ -0,0 +1,55 @@
+<script>
+ import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
+
+ export default {
+ props: {
+ documentationLink: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ iconCycleAnalyticsSplash() {
+ return iconCycleAnalyticsSplash;
+ },
+ },
+ methods: {
+ dismissOverviewDialog() {
+ this.$emit('dismiss-overview-dialog');
+ },
+ },
+ };
+</script>
+<template>
+ <div class="landing content-block">
+ <button
+ class="js-ca-dismiss-button dismiss-button"
+ type="button"
+ :aria-label="__('Dismiss Cycle Analytics introduction box')"
+ @click="dismissOverviewDialog">
+ <i
+ class="fa fa-times"
+ aria-hidden="true">
+ </i>
+ </button>
+ <div class="svg-container" v-html="iconCycleAnalyticsSplash">
+ </div>
+ <div class="inner-content">
+ <h4>
+ {{__('Introducing Cycle Analytics')}}
+ </h4>
+ <p>
+ {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }}
+ </p>
+ <p>
+ <a
+ :href="documentationLink"
+ target="_blank"
+ rel="nofollow"
+ class="btn">
+ {{__('Read more')}}
+ </a>
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue
index 9941b997b3f..62efd4f9c28 100644
--- a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue
@@ -20,7 +20,7 @@
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
- <template v-if="time.seconds && hasDa === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
+ <template v-if="time.seconds && hasData === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
</template>
<template v-else>
--
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 991fcf114da..49bb6c52180 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -1,8 +1,8 @@
-/* global Flash */
-
import Vue from 'vue';
import Cookies from 'js-cookie';
+import Flash from '../flash';
import Translate from '../vue_shared/translate';
+import banner from './components/banner.vue';
import stageCodeComponent from './components/stage_code_component.vue';
import stagePlanComponent from './components/stage_plan_component.vue';
import stageComponent from './components/stage_component.vue';
@@ -44,6 +44,7 @@ $(() => {
},
},
components: {
+ banner,
'stage-issue-component': stageComponent,
'stage-plan-component': stagePlanComponent,
'stage-code-component': stageCodeComponent,
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index a663e30dfd0..54e13b79a4f 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,5 +1,5 @@
<script>
- /* global Flash */
+ import Flash from '../../flash';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index ae8338f5fd2..6c78662baa7 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -3,6 +3,7 @@
import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
+import imageDiffHelper from './image_diff/helpers/index';
const UNFOLD_COUNT = 20;
let isBound = false;
@@ -17,9 +18,12 @@ class Diff {
}
});
- FilesCommentButton.init($diffFile);
+ const tab = document.getElementById('diffs');
+ if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile);
- $diffFile.each((index, file) => new gl.ImageFile(file));
+ const firstFile = $('.files').first().get(0);
+ const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note');
+ $diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote));
if (!isBound) {
$(document)
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 497c23f014f..e77910a83d4 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -171,7 +171,14 @@ const JumpToDiscussion = Vue.extend({
// When jumping between unresolved discussions on the diffs tab, we show them.
$target.closest(".content").show();
- $target = $target.closest("tr.notes_holder");
+ const $notesHolder = $target.closest("tr.notes_holder");
+
+ // Image diff discussions does not use notes_holder
+ // so we should keep original $target value in those cases
+ if ($notesHolder.length > 0) {
+ $target = $notesHolder;
+ }
+
$target.show();
// If we are on the diffs tab, we don't scroll to the discussion itself, but to
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index efb6ced9f46..20ddcbfb8bd 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -1,9 +1,9 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */
/* global CommentsStore */
/* global ResolveService */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../../flash';
const ResolveBtn = Vue.extend({
props: {
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index 2f063f6fe1f..6eae54f830b 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -1,7 +1,7 @@
-/* global Flash */
/* global CommentsStore */
import Vue from 'vue';
+import Flash from '../../flash';
import '../../vue_shared/vue_resource_interceptor';
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 4f9849d72f6..225cafbb7d4 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -7,14 +7,13 @@
/* global IssuableForm */
/* global LabelsSelect */
/* global MilestoneSelect */
-/* global Commit */
-/* global CommitsList */
/* global NewBranchForm */
/* global NotificationsForm */
/* global NotificationsDropdown */
/* global GroupAvatar */
/* global LineHighlighter */
-/* global BuildArtifacts */
+import BuildArtifacts from './build_artifacts';
+import CILintEditor from './ci_lint_editor';
/* global GroupsSelect */
/* global Search */
/* global Admin */
@@ -36,6 +35,7 @@
/* global Sidebar */
/* global ShortcutsWiki */
+import CommitsList from './commits';
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
import DeleteModal from './branches/branches_delete_modal';
@@ -77,7 +77,10 @@ import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown';
import NewGroupChild from './groups/new_group_child';
+import AbuseReports from './abuse_reports';
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
+import AjaxLoadingSpinner from './ajax_loading_spinner';
+import U2FAuthenticate from './u2f/authenticate';
(function() {
var Dispatcher;
@@ -90,8 +93,8 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
}
Dispatcher.prototype.initPageScripts = function() {
- var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
- page = $('body').attr('data-page');
+ var path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
+ const page = $('body').attr('data-page');
if (!page) {
return false;
}
@@ -239,7 +242,7 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML));
break;
case 'projects:branches:index':
- gl.AjaxLoadingSpinner.init();
+ AjaxLoadingSpinner.init();
new DeleteModal();
break;
case 'projects:issues:new':
@@ -317,7 +320,6 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
new gl.Activities();
break;
case 'projects:commit:show':
- new Commit();
new gl.Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
@@ -511,7 +513,7 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
break;
case 'ci:lints:create':
case 'ci:lints:show':
- new gl.CILintEditor();
+ new CILintEditor();
break;
case 'users:show':
new UserCallout();
@@ -531,19 +533,26 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
case 'admin:impersonation_tokens:index':
new gl.DueDateSelectors();
break;
+ case 'projects:clusters:show':
+ import(/* webpackChunkName: "clusters" */ './clusters')
+ .then(cluster => new cluster.default()) // eslint-disable-line new-cap
+ .catch(() => {});
+ break;
}
switch (path[0]) {
case 'sessions':
case 'omniauth_callbacks':
if (!gon.u2f) break;
- gl.u2fAuthenticate = new gl.U2FAuthenticate(
+ const u2fAuthenticate = new U2FAuthenticate(
$('#js-authenticate-u2f'),
'#js-login-u2f-form',
gon.u2f,
document.querySelector('#js-login-2fa-device'),
document.querySelector('.js-2fa-form'),
);
- gl.u2fAuthenticate.start();
+ u2fAuthenticate.start();
+ // needed in rspec
+ gl.u2fAuthenticate = u2fAuthenticate;
case 'admin':
new Admin();
switch (path[1]) {
@@ -563,7 +572,7 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
new Labels();
}
case 'abuse_reports':
- new gl.AbuseReports();
+ new AbuseReports();
break;
}
break;
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 1cba65d17cd..bd45da8c422 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -237,9 +237,12 @@ window.DropzoneInput = (function() {
};
const insertToTextArea = function(filename, url) {
- return $(child).val(function(index, val) {
+ const $child = $(child);
+ $child.val(function(index, val) {
return val.replace(`{{${filename}}}`, url);
});
+
+ $child.trigger('change');
};
const appendToTextArea = function(url) {
diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue
index ce5f6219a3e..c039ae85cfb 100644
--- a/app/assets/javascripts/environments/components/environment.vue
+++ b/app/assets/javascripts/environments/components/environment.vue
@@ -1,6 +1,6 @@
<script>
-/* global Flash */
import Visibility from 'visibilityjs';
+import Flash from '../../flash';
import EnvironmentsService from '../services/environments_service';
import environmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 01e70c0bbb7..b155560df9d 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,6 +1,6 @@
<script>
-/* global Flash */
import Visibility from 'visibilityjs';
+import Flash from '../../flash';
import EnvironmentsService from '../services/environments_service';
import environmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index ada14d2053c..a6cc079d720 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -1,7 +1,6 @@
-/* global Flash */
-
-import Ajax from '~/droplab/plugins/ajax';
-import Filter from '~/droplab/plugins/filter';
+import Flash from '../flash';
+import Ajax from '../droplab/plugins/ajax';
+import Filter from '../droplab/plugins/filter';
import './filtered_search_dropdown';
class DropdownEmoji extends gl.FilteredSearchDropdown {
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index b32d589481d..788fb1dc614 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -1,7 +1,6 @@
-/* global Flash */
-
-import Ajax from '~/droplab/plugins/ajax';
-import Filter from '~/droplab/plugins/filter';
+import Flash from '../flash';
+import Ajax from '../droplab/plugins/ajax';
+import Filter from '../droplab/plugins/filter';
import './filtered_search_dropdown';
class DropdownNonUser extends gl.FilteredSearchDropdown {
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index ce8817b1b2e..a9e2b65def0 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -1,6 +1,5 @@
-/* global Flash */
-
-import AjaxFilter from '~/droplab/plugins/ajax_filter';
+import Flash from '../flash';
+import AjaxFilter from '../droplab/plugins/ajax_filter';
import './filtered_search_dropdown';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index a44dc279a6f..7b233842d5a 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,3 +1,4 @@
+import Flash from '../flash';
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
@@ -36,7 +37,7 @@ class FilteredSearchManager {
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
- new window.Flash('An error occurred while parsing recent searches');
+ new Flash('An error occurred while parsing recent searches');
// Gracefully fail to empty array
return [];
})
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 28e8240169d..dd24fc44d2a 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,5 +1,5 @@
import AjaxCache from '../lib/utils/ajax_cache';
-import '../flash'; /* global Flash */
+import Flash from '../flash';
import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache';
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index ccff8f0ace7..bc5cd818e1c 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,71 +1,94 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */
+import _ from 'underscore';
-window.Flash = (function() {
- var hideFlash;
+const hideFlash = (flashEl, fadeTransition = true) => {
+ if (fadeTransition) {
+ Object.assign(flashEl.style, {
+ transition: 'opacity .3s',
+ opacity: '0',
+ });
+ }
- hideFlash = function() {
- return $(this).fadeOut();
- };
+ flashEl.addEventListener('transitionend', () => {
+ flashEl.remove();
+ }, {
+ once: true,
+ passive: true,
+ });
- /**
- * 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';
- }
- if (parent == null) {
- parent = null;
- }
- if (parent) {
- this.flashContainer = parent.find('.flash-container');
- } else {
- this.flashContainer = $('.flash-container-page');
- }
- this.flashContainer.html('');
- flash = $('<div/>', {
- "class": "flash-" + type
- });
- flash.on('click', hideFlash);
- textDiv = $('<div/>', {
- "class": 'flash-text',
- text: message
- });
- textDiv.appendTo(flash);
+ if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend'));
+};
- if (actionConfig) {
- const actionLinkConfig = {
- class: 'flash-action',
- href: actionConfig.href || '#',
- text: actionConfig.title
- };
+const createAction = config => `
+ <a
+ href="${config.href || '#'}"
+ class="flash-action"
+ ${config.href ? '' : 'role="button"'}
+ >
+ ${_.escape(config.title)}
+ </a>
+`;
- if (!actionConfig.href) {
- actionLinkConfig.role = 'button';
- }
+const createFlashEl = (message, type, isInContentWrapper = false) => `
+ <div
+ class="flash-${type}"
+ >
+ <div
+ class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}"
+ >
+ ${_.escape(message)}
+ </div>
+ </div>
+`;
- actionLink = $('<a/>', actionLinkConfig);
+/*
+ * 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 text
+ * @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} actonConfig Map of config to show action on banner
+ * @param {String} href URL to which action config should point to (default: '#')
+ * @param {String} title Title of action
+ * @param {Function} clickHandler Method to call when action is clicked on
+ * @param {Boolean} fadeTransition Boolean to determine whether to fade the alert out
+ */
+const createFlash = function createFlash(
+ message,
+ type = 'alert',
+ parent = document,
+ actionConfig = null,
+ fadeTransition = true,
+) {
+ const flashContainer = parent.querySelector('.flash-container');
- actionLink.appendTo(flash);
- this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler);
- }
- if (this.flashContainer.parent().hasClass('content-wrapper')) {
- textDiv.addClass('container-fluid container-limited');
+ if (!flashContainer) return null;
+
+ const isInContentWrapper = flashContainer.parentNode.classList.contains('content-wrapper');
+
+ flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper);
+
+ const flashEl = flashContainer.querySelector(`.flash-${type}`);
+ flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
+
+ if (actionConfig) {
+ flashEl.innerHTML += createAction(actionConfig);
+
+ if (actionConfig.clickHandler) {
+ flashEl.querySelector('.flash-action').addEventListener('click', e => actionConfig.clickHandler(e));
}
- flash.appendTo(this.flashContainer);
- this.flashContainer.show();
}
- return Flash;
-})();
+ flashContainer.style.display = 'block';
+
+ return flashContainer;
+};
+
+export {
+ createFlash as default,
+ createFlashEl,
+ createAction,
+ hideFlash,
+};
+window.Flash = createFlash;
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index ff218ccad62..e8d8fef8579 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -738,7 +738,7 @@ GitLabDropdown = (function() {
: selectedObject.id;
if (isInput) {
field = $(this.el);
- } else if (value) {
+ } else if (value != null) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
@@ -746,7 +746,7 @@ GitLabDropdown = (function() {
return;
}
- if (el.hasClass(ACTIVE_CLASS)) {
+ if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
isMarking = false;
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
@@ -852,7 +852,7 @@ GitLabDropdown = (function() {
if (href && href !== '#') {
gl.utils.visitUrl(href);
} else {
- $el.first().trigger('click');
+ $el.trigger('click');
}
}
};
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index 84286c3572e..35b96620209 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -1,8 +1,6 @@
-/* global Flash */
-
import Vue from 'vue';
-
import Translate from '../vue_shared/translate';
+import Flash from '../flash';
import GroupFilterableList from './groups_filterable_list';
import GroupsStore from './store/groups_store';
import GroupsService from './service/groups_service';
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index dc170c60456..ea2e2205077 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,7 +1,16 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */
+import { highCountTrim } from '~/lib/utils/text_utility';
-$(document).on('todo:toggle', function(e, count) {
- var $todoPendingCount = $('.todos-count');
- $todoPendingCount.text(gl.text.highCountTrim(count));
- $todoPendingCount.toggleClass('hidden', count === 0);
+/**
+ * Updates todo counter when todos are toggled.
+ * When count is 0, we hide the badge.
+ *
+ * @param {jQuery.Event} e
+ * @param {String} count
+ */
+$(document).on('todo:toggle', (e, count) => {
+ const parsedCount = parseInt(count, 10);
+ const $todoPendingCount = $('.todos-count');
+
+ $todoPendingCount.text(highCountTrim(parsedCount));
+ $todoPendingCount.toggleClass('hidden', parsedCount === 0);
});
diff --git a/app/assets/javascripts/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js
new file mode 100644
index 00000000000..6a6a668308d
--- /dev/null
+++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js
@@ -0,0 +1,38 @@
+export function createImageBadge(noteId, { x, y }, classNames = []) {
+ const buttonEl = document.createElement('button');
+ const classList = classNames.concat(['js-image-badge']);
+ classList.forEach(className => buttonEl.classList.add(className));
+ buttonEl.setAttribute('type', 'button');
+ buttonEl.setAttribute('disabled', true);
+ buttonEl.dataset.noteId = noteId;
+ buttonEl.style.left = `${x}px`;
+ buttonEl.style.top = `${y}px`;
+
+ return buttonEl;
+}
+
+export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
+ const buttonEl = createImageBadge(noteId, coordinate, ['badge']);
+ buttonEl.innerText = badgeText;
+
+ containerEl.appendChild(buttonEl);
+}
+
+export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
+ const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge', 'inverted']);
+ const iconEl = document.createElement('i');
+ iconEl.className = 'fa fa-comment-o';
+ iconEl.setAttribute('aria-label', 'comment');
+
+ buttonEl.appendChild(iconEl);
+ containerEl.appendChild(buttonEl);
+}
+
+export function addAvatarBadge(el, event) {
+ const { noteId, badgeNumber } = event.detail;
+
+ // Add badge to new comment
+ const avatarBadgeEl = el.querySelector(`#${noteId} .badge`);
+ avatarBadgeEl.innerText = badgeNumber;
+ avatarBadgeEl.classList.remove('hidden');
+}
diff --git a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
new file mode 100644
index 00000000000..05000c73052
--- /dev/null
+++ b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
@@ -0,0 +1,58 @@
+export function addCommentIndicator(containerEl, { x, y }) {
+ const buttonEl = document.createElement('button');
+ buttonEl.classList.add('btn-transparent');
+ buttonEl.classList.add('comment-indicator');
+ buttonEl.setAttribute('type', 'button');
+ buttonEl.style.left = `${x}px`;
+ buttonEl.style.top = `${y}px`;
+
+ buttonEl.innerHTML = gl.utils.spriteIcon('image-comment-dark');
+
+ containerEl.appendChild(buttonEl);
+}
+
+export function removeCommentIndicator(imageFrameEl) {
+ const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
+ const imageEl = imageFrameEl.querySelector('img');
+ const willRemove = !!commentIndicatorEl;
+ let meta = {};
+
+ if (willRemove) {
+ meta = {
+ x: parseInt(commentIndicatorEl.style.left, 10),
+ y: parseInt(commentIndicatorEl.style.top, 10),
+ image: {
+ width: imageEl.width,
+ height: imageEl.height,
+ },
+ };
+
+ commentIndicatorEl.remove();
+ }
+
+ return Object.assign({}, meta, {
+ removed: willRemove,
+ });
+}
+
+export function showCommentIndicator(imageFrameEl, coordinate) {
+ const { x, y } = coordinate;
+ const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
+
+ if (commentIndicatorEl) {
+ commentIndicatorEl.style.left = `${x}px`;
+ commentIndicatorEl.style.top = `${y}px`;
+ } else {
+ addCommentIndicator(imageFrameEl, coordinate);
+ }
+}
+
+export function commentIndicatorOnClick(event) {
+ // Prevent from triggering onAddImageDiffNote in notes.js
+ event.stopPropagation();
+
+ const buttonEl = event.currentTarget;
+ const diffViewerEl = buttonEl.closest('.diff-viewer');
+ const textareaEl = diffViewerEl.querySelector('.note-container .note-textarea');
+ textareaEl.focus();
+}
diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js
new file mode 100644
index 00000000000..12d56714b34
--- /dev/null
+++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js
@@ -0,0 +1,44 @@
+export function setPositionDataAttribute(el, options) {
+ // Update position data attribute so that the
+ // new comment form can use this data for ajax request
+ const { x, y, width, height } = options;
+ const position = el.dataset.position;
+ const positionObject = Object.assign({}, JSON.parse(position), {
+ x,
+ y,
+ width,
+ height,
+ });
+
+ el.setAttribute('data-position', JSON.stringify(positionObject));
+}
+
+export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {
+ const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .badge');
+ avatarBadgeEl.innerText = newBadgeNumber;
+}
+
+export function updateDiscussionBadgeNumber(discussionEl, newBadgeNumber) {
+ const discussionBadgeEl = discussionEl.querySelector('.badge');
+ discussionBadgeEl.innerText = newBadgeNumber;
+}
+
+export function toggleCollapsed(event) {
+ const toggleButtonEl = event.currentTarget;
+ const discussionNotesEl = toggleButtonEl.closest('.discussion-notes');
+ const formEl = discussionNotesEl.querySelector('.discussion-form');
+ const isCollapsed = discussionNotesEl.classList.contains('collapsed');
+
+ if (isCollapsed) {
+ discussionNotesEl.classList.remove('collapsed');
+ } else {
+ discussionNotesEl.classList.add('collapsed');
+ }
+
+ // Override the inline display style set in notes.js
+ if (formEl && !isCollapsed) {
+ formEl.style.display = 'none';
+ } else if (formEl && isCollapsed) {
+ formEl.style.display = 'block';
+ }
+}
diff --git a/app/assets/javascripts/image_diff/helpers/index.js b/app/assets/javascripts/image_diff/helpers/index.js
new file mode 100644
index 00000000000..4a100631003
--- /dev/null
+++ b/app/assets/javascripts/image_diff/helpers/index.js
@@ -0,0 +1,25 @@
+import * as badgeHelper from './badge_helper';
+import * as commentIndicatorHelper from './comment_indicator_helper';
+import * as domHelper from './dom_helper';
+import * as utilsHelper from './utils_helper';
+
+export default {
+ addCommentIndicator: commentIndicatorHelper.addCommentIndicator,
+ removeCommentIndicator: commentIndicatorHelper.removeCommentIndicator,
+ showCommentIndicator: commentIndicatorHelper.showCommentIndicator,
+ commentIndicatorOnClick: commentIndicatorHelper.commentIndicatorOnClick,
+
+ addImageBadge: badgeHelper.addImageBadge,
+ addImageCommentBadge: badgeHelper.addImageCommentBadge,
+ addAvatarBadge: badgeHelper.addAvatarBadge,
+
+ setPositionDataAttribute: domHelper.setPositionDataAttribute,
+ updateDiscussionAvatarBadgeNumber: domHelper.updateDiscussionAvatarBadgeNumber,
+ updateDiscussionBadgeNumber: domHelper.updateDiscussionBadgeNumber,
+ toggleCollapsed: domHelper.toggleCollapsed,
+
+ resizeCoordinatesToImageElement: utilsHelper.resizeCoordinatesToImageElement,
+ generateBadgeFromDiscussionDOM: utilsHelper.generateBadgeFromDiscussionDOM,
+ getTargetSelection: utilsHelper.getTargetSelection,
+ initImageDiff: utilsHelper.initImageDiff,
+};
diff --git a/app/assets/javascripts/image_diff/helpers/utils_helper.js b/app/assets/javascripts/image_diff/helpers/utils_helper.js
new file mode 100644
index 00000000000..96fc735e629
--- /dev/null
+++ b/app/assets/javascripts/image_diff/helpers/utils_helper.js
@@ -0,0 +1,95 @@
+import ImageBadge from '../image_badge';
+import ImageDiff from '../image_diff';
+import ReplacedImageDiff from '../replaced_image_diff';
+import '../../commit/image_file';
+
+export function resizeCoordinatesToImageElement(imageEl, meta) {
+ const { x, y, width, height } = meta;
+
+ const imageWidth = imageEl.width;
+ const imageHeight = imageEl.height;
+
+ const widthRatio = imageWidth / width;
+ const heightRatio = imageHeight / height;
+
+ return {
+ x: Math.round(x * widthRatio),
+ y: Math.round(y * heightRatio),
+ width: imageWidth,
+ height: imageHeight,
+ };
+}
+
+export function generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl) {
+ const position = JSON.parse(discussionEl.dataset.position);
+ const firstNoteEl = discussionEl.querySelector('.note');
+ const badge = new ImageBadge({
+ actual: position,
+ imageEl: imageFrameEl.querySelector('img'),
+ noteId: firstNoteEl.id,
+ discussionId: discussionEl.dataset.discussionId,
+ });
+
+ return badge;
+}
+
+export function getTargetSelection(event) {
+ const containerEl = event.currentTarget;
+ const imageEl = containerEl.querySelector('img');
+
+ const x = event.offsetX;
+ const y = event.offsetY;
+
+ const width = imageEl.width;
+ const height = imageEl.height;
+
+ const actualWidth = imageEl.naturalWidth;
+ const actualHeight = imageEl.naturalHeight;
+
+ const widthRatio = actualWidth / width;
+ const heightRatio = actualHeight / height;
+
+ // Browser will include the frame as a clickable target,
+ // which would result in potential 1px out of bounds value
+ // This bound the coordinates to inside the frame
+ const normalizedX = Math.max(0, x) && Math.min(x, width);
+ const normalizedY = Math.max(0, y) && Math.min(y, height);
+
+ return {
+ browser: {
+ x: normalizedX,
+ y: normalizedY,
+ width,
+ height,
+ },
+ actual: {
+ // Round x, y so that we don't need to deal with decimals
+ x: Math.round(normalizedX * widthRatio),
+ y: Math.round(normalizedY * heightRatio),
+ width: actualWidth,
+ height: actualHeight,
+ },
+ };
+}
+
+export function initImageDiff(fileEl, canCreateNote, renderCommentBadge) {
+ const options = {
+ canCreateNote,
+ renderCommentBadge,
+ };
+ let diff;
+
+ // ImageFile needs to be invoked before initImageDiff so that badges
+ // can mount to the correct location
+ new gl.ImageFile(fileEl); // eslint-disable-line no-new
+
+ if (fileEl.querySelector('.diff-file .js-single-image')) {
+ diff = new ImageDiff(fileEl, options);
+ diff.init();
+ } else if (fileEl.querySelector('.diff-file .js-replaced-image')) {
+ diff = new ReplacedImageDiff(fileEl, options);
+ diff.init();
+ }
+
+ return diff;
+}
diff --git a/app/assets/javascripts/image_diff/image_badge.js b/app/assets/javascripts/image_diff/image_badge.js
new file mode 100644
index 00000000000..51a8cda98d7
--- /dev/null
+++ b/app/assets/javascripts/image_diff/image_badge.js
@@ -0,0 +1,23 @@
+import imageDiffHelper from './helpers/index';
+
+const defaultMeta = {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+};
+
+export default class ImageBadge {
+ constructor(options) {
+ const { noteId, discussionId } = options;
+
+ this.actual = options.actual || defaultMeta;
+ this.browser = options.browser || defaultMeta;
+ this.noteId = noteId;
+ this.discussionId = discussionId;
+
+ if (options.imageEl && !options.browser) {
+ this.browser = imageDiffHelper.resizeCoordinatesToImageElement(options.imageEl, this.actual);
+ }
+ }
+}
diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js
new file mode 100644
index 00000000000..f3af92cf2b0
--- /dev/null
+++ b/app/assets/javascripts/image_diff/image_diff.js
@@ -0,0 +1,143 @@
+import imageDiffHelper from './helpers/index';
+import ImageBadge from './image_badge';
+import { isImageLoaded } from '../lib/utils/image_utility';
+
+export default class ImageDiff {
+ constructor(el, options) {
+ this.el = el;
+ this.canCreateNote = !!(options && options.canCreateNote);
+ this.renderCommentBadge = !!(options && options.renderCommentBadge);
+ this.$noteContainer = $('.note-container', this.el);
+ this.imageBadges = [];
+ }
+
+ init() {
+ this.imageFrameEl = this.el.querySelector('.diff-file .js-image-frame');
+ this.imageEl = this.imageFrameEl.querySelector('img');
+
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ this.imageClickedWrapper = this.imageClicked.bind(this);
+ this.imageBlurredWrapper = imageDiffHelper.removeCommentIndicator.bind(null, this.imageFrameEl);
+ this.addBadgeWrapper = this.addBadge.bind(this);
+ this.removeBadgeWrapper = this.removeBadge.bind(this);
+ this.renderBadgesWrapper = this.renderBadges.bind(this);
+
+ // Render badges
+ if (isImageLoaded(this.imageEl)) {
+ this.renderBadges();
+ } else {
+ this.imageEl.addEventListener('load', this.renderBadgesWrapper);
+ }
+
+ // jquery makes the event delegation here much simpler
+ this.$noteContainer.on('click', '.js-diff-notes-toggle', imageDiffHelper.toggleCollapsed);
+ $(this.el).on('click', '.comment-indicator', imageDiffHelper.commentIndicatorOnClick);
+
+ if (this.canCreateNote) {
+ this.el.addEventListener('click.imageDiff', this.imageClickedWrapper);
+ this.el.addEventListener('blur.imageDiff', this.imageBlurredWrapper);
+ this.el.addEventListener('addBadge.imageDiff', this.addBadgeWrapper);
+ this.el.addEventListener('removeBadge.imageDiff', this.removeBadgeWrapper);
+ }
+ }
+
+ imageClicked(event) {
+ const customEvent = event.detail;
+ const selection = imageDiffHelper.getTargetSelection(customEvent);
+ const el = customEvent.currentTarget;
+
+ imageDiffHelper.setPositionDataAttribute(el, selection.actual);
+ imageDiffHelper.showCommentIndicator(this.imageFrameEl, selection.browser);
+ }
+
+ renderBadges() {
+ const discussionsEls = this.el.querySelectorAll('.note-container .discussion-notes .notes');
+ [...discussionsEls].forEach(this.renderBadge.bind(this));
+ }
+
+ renderBadge(discussionEl, index) {
+ const imageBadge = imageDiffHelper
+ .generateBadgeFromDiscussionDOM(this.imageFrameEl, discussionEl);
+
+ this.imageBadges.push(imageBadge);
+
+ const options = {
+ coordinate: imageBadge.browser,
+ noteId: imageBadge.noteId,
+ };
+
+ if (this.renderCommentBadge) {
+ imageDiffHelper.addImageCommentBadge(this.imageFrameEl, options);
+ } else {
+ const numberBadgeOptions = Object.assign({}, options, {
+ badgeText: index + 1,
+ });
+
+ imageDiffHelper.addImageBadge(this.imageFrameEl, numberBadgeOptions);
+ }
+ }
+
+ addBadge(event) {
+ const { x, y, width, height, noteId, discussionId } = event.detail;
+ const badgeText = this.imageBadges.length + 1;
+ const imageBadge = new ImageBadge({
+ actual: {
+ x,
+ y,
+ width,
+ height,
+ },
+ imageEl: this.imageFrameEl.querySelector('img'),
+ noteId,
+ discussionId,
+ });
+
+ this.imageBadges.push(imageBadge);
+
+ imageDiffHelper.addImageBadge(this.imageFrameEl, {
+ coordinate: imageBadge.browser,
+ badgeText,
+ noteId,
+ });
+
+ imageDiffHelper.addAvatarBadge(this.el, {
+ detail: {
+ noteId,
+ badgeNumber: badgeText,
+ },
+ });
+
+ const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
+ imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, badgeText);
+ }
+
+ removeBadge(event) {
+ const { badgeNumber } = event.detail;
+ const indexToRemove = badgeNumber - 1;
+ const imageBadgeEls = this.imageFrameEl.querySelectorAll('.badge');
+
+ if (this.imageBadges.length !== badgeNumber) {
+ // Cascade badges count numbers for (avatar badges + image badges)
+ this.imageBadges.forEach((badge, index) => {
+ if (index > indexToRemove) {
+ const { discussionId } = badge;
+ const updatedBadgeNumber = index;
+ const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
+
+ imageBadgeEls[index].innerText = updatedBadgeNumber;
+
+ imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, updatedBadgeNumber);
+ imageDiffHelper.updateDiscussionAvatarBadgeNumber(discussionEl, updatedBadgeNumber);
+ }
+ });
+ }
+
+ this.imageBadges.splice(indexToRemove, 1);
+
+ const imageBadgeEl = imageBadgeEls[indexToRemove];
+ imageBadgeEl.remove();
+ }
+}
diff --git a/app/assets/javascripts/image_diff/init_discussion_tab.js b/app/assets/javascripts/image_diff/init_discussion_tab.js
new file mode 100644
index 00000000000..2f16c6ef115
--- /dev/null
+++ b/app/assets/javascripts/image_diff/init_discussion_tab.js
@@ -0,0 +1,12 @@
+import imageDiffHelper from './helpers/index';
+
+export default () => {
+ // Always pass can-create-note as false because a user
+ // cannot place new badge markers on discussion tab
+ const canCreateNote = false;
+ const renderCommentBadge = true;
+
+ const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file');
+ [...diffFileEls].forEach(diffFileEl =>
+ imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge));
+};
diff --git a/app/assets/javascripts/image_diff/replaced_image_diff.js b/app/assets/javascripts/image_diff/replaced_image_diff.js
new file mode 100644
index 00000000000..4abd13fb472
--- /dev/null
+++ b/app/assets/javascripts/image_diff/replaced_image_diff.js
@@ -0,0 +1,92 @@
+import imageDiffHelper from './helpers/index';
+import { viewTypes, isValidViewType } from './view_types';
+import ImageDiff from './image_diff';
+
+export default class ReplacedImageDiff extends ImageDiff {
+ init(defaultViewType = viewTypes.TWO_UP) {
+ this.imageFrameEls = {
+ [viewTypes.TWO_UP]: this.el.querySelector('.two-up .js-image-frame'),
+ [viewTypes.SWIPE]: this.el.querySelector('.swipe .js-image-frame'),
+ [viewTypes.ONION_SKIN]: this.el.querySelector('.onion-skin .js-image-frame'),
+ };
+
+ const viewModesEl = this.el.querySelector('.view-modes-menu');
+ this.viewModesEls = {
+ [viewTypes.TWO_UP]: viewModesEl.querySelector('.two-up'),
+ [viewTypes.SWIPE]: viewModesEl.querySelector('.swipe'),
+ [viewTypes.ONION_SKIN]: viewModesEl.querySelector('.onion-skin'),
+ };
+
+ this.currentView = defaultViewType;
+ this.generateImageEls();
+ this.bindEvents();
+ }
+
+ generateImageEls() {
+ this.imageEls = {};
+
+ const viewTypeNames = Object.getOwnPropertyNames(viewTypes);
+ viewTypeNames.forEach((viewType) => {
+ this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img');
+ });
+ }
+
+ bindEvents() {
+ super.bindEvents();
+
+ this.changeToViewTwoUp = this.changeView.bind(this, viewTypes.TWO_UP);
+ this.changeToViewSwipe = this.changeView.bind(this, viewTypes.SWIPE);
+ this.changeToViewOnionSkin = this.changeView.bind(this, viewTypes.ONION_SKIN);
+
+ this.viewModesEls[viewTypes.TWO_UP].addEventListener('click', this.changeToViewTwoUp);
+ this.viewModesEls[viewTypes.SWIPE].addEventListener('click', this.changeToViewSwipe);
+ this.viewModesEls[viewTypes.ONION_SKIN].addEventListener('click', this.changeToViewOnionSkin);
+ }
+
+ get imageEl() {
+ return this.imageEls[this.currentView];
+ }
+
+ get imageFrameEl() {
+ return this.imageFrameEls[this.currentView];
+ }
+
+ changeView(newView) {
+ if (!isValidViewType(newView)) {
+ return;
+ }
+
+ const indicator = imageDiffHelper.removeCommentIndicator(this.imageFrameEl);
+
+ this.currentView = newView;
+
+ // Clear existing badges on new view
+ const existingBadges = this.imageFrameEl.querySelectorAll('.badge');
+ [...existingBadges].map(badge => badge.remove());
+
+ // Remove existing references to old view image badges
+ this.imageBadges = [];
+
+ // Image_file.js has a fade animation of 200ms for loading the view
+ // Need to wait an additional 250ms for the images to be displayed
+ // on window in order to re-normalize their dimensions
+ setTimeout(this.renderNewView.bind(this, indicator), 250);
+ }
+
+ renderNewView(indicator) {
+ // Generate badge coordinates on new view
+ this.renderBadges();
+
+ // Re-render indicator in new view
+ if (indicator.removed) {
+ const normalizedIndicator = imageDiffHelper
+ .resizeCoordinatesToImageElement(this.imageEl, {
+ x: indicator.x,
+ y: indicator.y,
+ width: indicator.image.width,
+ height: indicator.image.height,
+ });
+ imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator);
+ }
+ }
+}
diff --git a/app/assets/javascripts/image_diff/view_types.js b/app/assets/javascripts/image_diff/view_types.js
new file mode 100644
index 00000000000..ab0a595571f
--- /dev/null
+++ b/app/assets/javascripts/image_diff/view_types.js
@@ -0,0 +1,9 @@
+export const viewTypes = {
+ TWO_UP: 'TWO_UP',
+ SWIPE: 'SWIPE',
+ ONION_SKIN: 'ONION_SKIN',
+};
+
+export function isValidViewType(validate) {
+ return !!Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate);
+}
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index cf1e6a14725..32415a8791f 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,4 +1,4 @@
-/* global Flash */
+import Flash from '../flash';
export default class IntegrationSettingsForm {
constructor(formSelector) {
@@ -102,7 +102,7 @@ export default class IntegrationSettingsForm {
})
.done((res) => {
if (res.error) {
- new Flash(`${res.message} ${res.service_response}`, null, null, {
+ new Flash(`${res.message} ${res.service_response}`, 'alert', document, {
title: 'Save anyway',
clickHandler: (e) => {
e.preventDefault();
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index c39ffdb2e0f..eb15949603f 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global IssuableIndex */
-/* global Flash */
import _ from 'underscore';
+import Flash from './flash';
export default {
init({ container, form, issues, prefixId } = {}) {
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index c0bd64814ca..3fc29f9a661 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,9 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
-/* global Flash */
-
import 'vendor/jquery.waitforimages';
import '~/lib/utils/text_utility';
-import './flash';
+import Flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 06f6ec241f4..eecb56cb185 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -1,5 +1,4 @@
<script>
-/* global Flash */
import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
@@ -153,7 +152,7 @@ export default {
})
.catch(() => {
eventHub.$emit('close.form');
- return new Flash('Error updating issue');
+ window.Flash('Error updating issue');
});
},
deleteIssuable() {
@@ -167,7 +166,7 @@ export default {
})
.catch(() => {
eventHub.$emit('close.form');
- return new Flash('Error deleting issue');
+ window.Flash('Error deleting issue');
});
},
},
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index dc902eefc5f..0aa1b2c2e31 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -1,5 +1,4 @@
<script>
- /* global Flash */
import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue';
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/job.js
index 3d27a3544eb..c6b5844dff6 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/job.js
@@ -1,15 +1,12 @@
-/* eslint-disable func-names, wrap-iife, no-use-before-define,
-consistent-return, prefer-rest-params */
import _ from 'underscore';
import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
-window.Build = (function () {
- Build.timeout = null;
- Build.state = null;
-
- function Build(options) {
+export default class Job {
+ constructor(options) {
+ this.timeout = null;
+ this.state = null;
this.options = options || $('.js-build-options').data();
this.pageUrl = this.options.pageUrl;
@@ -19,9 +16,7 @@ window.Build = (function () {
this.$document = $(document);
this.logBytes = 0;
this.hasBeenScrolled = false;
-
this.updateDropdown = this.updateDropdown.bind(this);
- this.getBuildTrace = this.getBuildTrace.bind(this);
this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh');
@@ -33,7 +28,7 @@ window.Build = (function () {
this.$scrollTopBtn = $('.js-scroll-up');
this.$scrollBottomBtn = $('.js-scroll-down');
- clearTimeout(Build.timeout);
+ clearTimeout(this.timeout);
this.initSidebar();
this.populateJobs(this.buildStage);
@@ -85,7 +80,7 @@ window.Build = (function () {
this.getBuildTrace();
}
- Build.prototype.initAffixTopArea = function () {
+ initAffixTopArea() {
/**
If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not
@@ -100,13 +95,14 @@ window.Build = (function () {
top: offsetTop,
},
});
- };
+ }
- Build.prototype.canScroll = function () {
+ // eslint-disable-next-line class-methods-use-this
+ canScroll() {
return $(document).height() > $(window).height();
- };
+ }
- Build.prototype.toggleScroll = function () {
+ toggleScroll() {
const currentPosition = $(document).scrollTop();
const scrollHeight = $(document).height();
@@ -119,7 +115,7 @@ window.Build = (function () {
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (currentPosition === 0) {
- // User is at Top of Build Log
+ // User is at Top of Log
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
@@ -133,38 +129,40 @@ window.Build = (function () {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, true);
}
- };
+ }
- Build.prototype.scrollDown = function () {
+ // eslint-disable-next-line class-methods-use-this
+ scrollDown() {
$(document).scrollTop($(document).height());
- };
+ }
- Build.prototype.scrollToBottom = function () {
+ scrollToBottom() {
this.scrollDown();
this.hasBeenScrolled = true;
this.toggleScroll();
- };
+ }
- Build.prototype.scrollToTop = function () {
+ scrollToTop() {
$(document).scrollTop(0);
this.hasBeenScrolled = true;
this.toggleScroll();
- };
+ }
- Build.prototype.toggleDisableButton = function ($button, disable) {
+ // eslint-disable-next-line class-methods-use-this
+ toggleDisableButton($button, disable) {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
- };
+ }
- Build.prototype.toggleScrollAnimation = function (toggle) {
+ toggleScrollAnimation(toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle);
- };
+ }
- Build.prototype.initSidebar = function () {
+ initSidebar() {
this.$sidebar = $('.js-build-sidebar');
- };
+ }
- Build.prototype.getBuildTrace = function () {
+ getBuildTrace() {
return $.ajax({
url: `${this.pageUrl}/trace.json`,
data: { state: this.state },
@@ -204,7 +202,7 @@ window.Build = (function () {
this.toggleScrollAnimation(false);
}
- Build.timeout = setTimeout(() => {
+ this.timeout = setTimeout(() => {
this.getBuildTrace();
}, 4000);
} else {
@@ -225,14 +223,14 @@ window.Build = (function () {
}
})
.then(() => this.toggleScroll());
- };
-
- Build.prototype.shouldHideSidebarForViewport = function () {
+ }
+ // eslint-disable-next-line class-methods-use-this
+ shouldHideSidebarForViewport() {
const bootstrapBreakpoint = bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
- };
+ }
- Build.prototype.toggleSidebar = function (shouldHide) {
+ toggleSidebar(shouldHide) {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
const $toggleButton = $('.js-sidebar-build-toggle-header');
@@ -249,17 +247,17 @@ window.Build = (function () {
} else {
$toggleButton.removeClass('hidden');
}
- };
+ }
- Build.prototype.sidebarOnResize = function () {
+ sidebarOnResize() {
this.toggleSidebar(this.shouldHideSidebarForViewport());
- };
+ }
- Build.prototype.sidebarOnClick = function () {
+ sidebarOnClick() {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
- };
-
- Build.prototype.updateArtifactRemoveDate = function () {
+ }
+ // eslint-disable-next-line class-methods-use-this, consistent-return
+ updateArtifactRemoveDate() {
const $date = $('.js-artifacts-remove');
if ($date.length) {
const date = $date.text();
@@ -267,23 +265,21 @@ window.Build = (function () {
gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '),
);
}
- };
-
- Build.prototype.populateJobs = function (stage) {
+ }
+ // eslint-disable-next-line class-methods-use-this
+ populateJobs(stage) {
$('.build-job').hide();
$(`.build-job[data-stage="${stage}"]`).show();
- };
-
- Build.prototype.updateStageDropdownText = function (stage) {
+ }
+ // eslint-disable-next-line class-methods-use-this
+ updateStageDropdownText(stage) {
$('.stage-selection').text(stage);
- };
+ }
- Build.prototype.updateDropdown = function (e) {
+ updateDropdown(e) {
e.preventDefault();
const stage = e.currentTarget.text;
this.updateStageDropdownText(stage);
this.populateJobs(stage);
- };
-
- return Build;
-})();
+ }
+}
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index f92e669414a..baaf5641200 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -1,5 +1,3 @@
-/* global Flash */
-
import Vue from 'vue';
import JobMediator from './job_details_mediator';
import jobHeader from './components/header.vue';
diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js
index cc014b815c4..3e2658f9fc1 100644
--- a/app/assets/javascripts/jobs/job_details_mediator.js
+++ b/app/assets/javascripts/jobs/job_details_mediator.js
@@ -1,11 +1,12 @@
-/* global Flash */
/* global Build */
import Visibility from 'visibilityjs';
+import Flash from '../flash';
import Poll from '../lib/utils/poll';
import JobStore from './stores/job_store';
import JobService from './services/job_service';
-import '../build';
+import Job from '../job';
+import handleRevealVariables from '../build_variables';
export default class JobMediator {
constructor(options = {}) {
@@ -20,7 +21,8 @@ export default class JobMediator {
}
initBuildClass() {
- this.build = new Build();
+ this.build = new Job();
+ handleRevealVariables();
}
fetchJob() {
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index d8814802d9e..a8f613c6cf2 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -1,7 +1,8 @@
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
-/* global Flash */
/* global Sortable */
+import Flash from './flash';
+
((global) => {
class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 2538d9c2093..d479f7ed682 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,6 +4,7 @@
import _ from 'underscore';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
+import CreateLabelDropdown from './create_label';
(function() {
this.LabelsSelect = (function() {
@@ -61,7 +62,7 @@ import DropdownUtils from './filtered_search/dropdown_utils';
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
- new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
+ new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
}
saveLabelData = function() {
diff --git a/app/assets/javascripts/lib/utils/csrf.js b/app/assets/javascripts/lib/utils/csrf.js
index ae41cc5e8a8..0bdb547d31a 100644
--- a/app/assets/javascripts/lib/utils/csrf.js
+++ b/app/assets/javascripts/lib/utils/csrf.js
@@ -14,6 +14,9 @@ If you need to compose a headers object, use the spread operator:
someOtherHeader: '12345',
}
```
+
+see also http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
+and https://github.com/rails/jquery-rails/blob/v4.3.1/vendor/assets/javascripts/jquery_ujs.js#L59-L62
*/
const csrf = {
@@ -53,4 +56,3 @@ if ($.rails) {
}
export default csrf;
-
diff --git a/app/assets/javascripts/lib/utils/image_utility.js b/app/assets/javascripts/lib/utils/image_utility.js
new file mode 100644
index 00000000000..2977ec821cb
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/image_utility.js
@@ -0,0 +1,5 @@
+/* eslint-disable import/prefer-default-export */
+
+export function isImageLoaded(element) {
+ return element.complete && element.naturalHeight !== 0;
+}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 021f936a4fa..f776829f69c 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
+/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
import 'vendor/latinise';
@@ -13,9 +13,17 @@ if ((base = w.gl).text == null) {
gl.text.addDelimiter = function(text) {
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
};
-gl.text.highCountTrim = function(count) {
+
+/**
+ * Returns '99+' for numbers bigger than 99.
+ *
+ * @param {Number} count
+ * @return {Number|String}
+ */
+export function highCountTrim(count) {
return count > 99 ? '99+' : count;
-};
+}
+
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 3328ff9cc23..78c7a094127 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,4 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
+
var base;
var w = window;
if (w.gl == null) {
@@ -86,6 +87,21 @@ w.gl.utils.getLocationHash = function(url) {
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
-w.gl.utils.visitUrl = (url) => {
- document.location.href = url;
+// eslint-disable-next-line import/prefer-default-export
+export function visitUrl(url, external = false) {
+ if (external) {
+ // Simulate `target="blank" rel="noopener noreferrer"`
+ // See https://mathiasbynens.github.io/rel-noopener/
+ const otherWindow = window.open();
+ otherWindow.opener = null;
+ otherWindow.location = url;
+ } else {
+ document.location.href = url;
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.utils = {
+ ...(window.gl.utils || {}),
+ visitUrl,
};
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index a16d00b5cef..a75d1a4b8d0 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -54,12 +54,14 @@ LineHighlighter.prototype.bindEvents = function() {
$fileHolder.on('highlight:line', this.highlightHash);
};
-LineHighlighter.prototype.highlightHash = function() {
- var range;
+LineHighlighter.prototype.highlightHash = function(newHash) {
+ let range;
+ if (newHash && typeof newHash === 'string') this._hash = newHash;
+
+ this.clearHighlight();
if (this._hash !== '') {
range = this.hashToRange(this._hash);
-
if (range[0]) {
this.highlightRange(range);
const lineSelector = `#L${range[0]}`;
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index ce05b3eabec..1003b9ba0af 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -4,6 +4,7 @@ import sprintf from './sprintf';
const langAttribute = document.querySelector('html').getAttribute('lang');
const lang = (langAttribute || 'en').replace(/-/g, '_');
const locale = new Jed(window.translations || {});
+delete window.translations;
/**
Translates `text`
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 24abc5c5c9e..50aa445e9e7 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,5 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
-/* global Flash */
/* global ConfirmDangerModal */
/* global Aside */
@@ -35,12 +34,9 @@ import './shortcuts_network';
import './templates/issuable_template_selector';
import './templates/issuable_template_selectors';
-// commit
-import './commit/file';
import './commit/image_file';
// lib/utils
-import './lib/utils/bootstrap_linked_tabs';
import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
import './lib/utils/pretty_time';
@@ -50,40 +46,25 @@ import './lib/utils/url_utility';
// behaviors
import './behaviors/';
-// u2f
-import './u2f/authenticate';
-import './u2f/error';
-import './u2f/register';
-import './u2f/util';
-
// everything else
-import './abuse_reports';
import './activities';
import './admin';
-import './ajax_loading_spinner';
-import './api';
import './aside';
import './autosave';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './broadcast_message';
-import './build';
-import './build_artifacts';
-import './build_variables';
-import './ci_lint_editor';
-import './commit';
import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
import './copy_as_gfm';
import './copy_to_clipboard';
-import './create_label';
import './diff';
import './dropzone_input';
import './due_date_select';
import './files_comment_button';
-import './flash';
+import Flash from './flash';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
@@ -111,7 +92,6 @@ import './merge_request';
import './merge_request_tabs';
import './milestone';
import './milestone_select';
-import './mini_pipeline_graph_dropdown';
import './namespace_select';
import './new_branch_form';
import './new_commit_form';
@@ -119,7 +99,6 @@ import './notes';
import './notifications_dropdown';
import './notifications_form';
import './pager';
-import './pipelines';
import './preview_markdown';
import './project';
import './project_avatar';
@@ -178,7 +157,6 @@ $(function () {
var $document = $(document);
var $window = $(window);
var $sidebarGutterToggle = $('.js-sidebar-toggle');
- var $flash = $('.flash-container');
var bootstrapBreakpoint = bp.getBreakpointSize();
var fitSidebarForSize;
@@ -263,13 +241,6 @@ $(function () {
// Form submitter
});
gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
- // Flash
- if ($flash.length > 0) {
- $flash.click(function () {
- return $(this).fadeOut();
- });
- $flash.show();
- }
// Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
var buttons;
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
index 645045fea88..93f8f6ee926 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
@@ -1,8 +1,8 @@
/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
/* global ace */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../../flash';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index d74cf5328ad..17591829b76 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,7 +1,7 @@
/* eslint-disable new-cap, comma-dangle, no-new */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
import './merge_conflict_service';
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index d3299c15720..df042c7baff 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,9 +1,8 @@
/* eslint-disable no-new, class-methods-use-this */
-/* global Flash */
/* global notes */
import Cookies from 'js-cookie';
-import './flash';
+import Flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
@@ -13,6 +12,8 @@ import {
isMetaClick,
} from './lib/utils/common_utils';
+import initDiscussionTab from './image_diff/init_discussion_tab';
+
/* eslint-disable max-len */
// MergeRequestTabs
//
@@ -154,6 +155,8 @@ import {
}
this.resetViewContainer();
this.destroyPipelinesView();
+
+ initDiscussionTab();
}
if (this.setUrl) {
this.setCurrentAction(action);
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 3e07ec4d0aa..8f3f1986763 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,7 +1,8 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
-/* global Flash */
/* global Sortable */
+import Flash from './flash';
+
(function() {
this.Milestone = (function() {
function Milestone() {
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index 64c1447f427..ca3d271663b 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -1,5 +1,5 @@
/* eslint-disable no-new */
-/* global Flash */
+import Flash from './flash';
/**
* In each pipelines table we have a mini pipeline graph for each pipeline.
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index f80a26b3fd4..cbe24c0915b 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,6 +1,6 @@
<script>
- /* global Flash */
import _ from 'underscore';
+ import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
import Graph from './graph.vue';
@@ -29,6 +29,7 @@
showEmptyState: true,
updateAspectRatio: false,
updatedAspectRatios: 0,
+ hoverData: {},
resizeThrottled: {},
};
},
@@ -64,6 +65,10 @@
this.updatedAspectRatios = 0;
}
},
+
+ hoverChanged(data) {
+ this.hoverData = data;
+ },
},
created() {
@@ -72,10 +77,12 @@
deploymentEndpoint: this.deploymentEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
+ eventHub.$on('hoverChanged', this.hoverChanged);
},
beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
+ eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false);
},
@@ -102,6 +109,7 @@
v-for="(graphData, index) in groupData.metrics"
:key="index"
:graph-data="graphData"
+ :hover-data="hoverData"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
/>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index a7b483f6786..a18164482a2 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -73,34 +73,22 @@
<template>
<div class="prometheus-state">
- <div class="row">
- <div class="col-md-4 col-md-offset-4 state-svg svg-content">
- <img :src="currentState.svgUrl"/>
- </div>
+ <div class="state-svg svg-content">
+ <img :src="currentState.svgUrl"/>
</div>
- <div class="row">
- <div class="col-md-6 col-md-offset-3">
- <h4 class="text-center state-title">
- {{currentState.title}}
- </h4>
- </div>
- </div>
- <div class="row">
- <div class="col-md-6 col-md-offset-3">
- <div class="description-text text-center state-description">
- {{currentState.description}}
- <a v-if="showButtonDescription" :href="settingsPath">
- Prometheus server
- </a>
- </div>
- </div>
- </div>
- <div class="row state-button-section">
- <div class="col-md-4 col-md-offset-4 text-center state-button">
- <a class="btn btn-success" :href="buttonPath">
- {{currentState.buttonText}}
- </a>
- </div>
+ <h4 class="state-title">
+ {{currentState.title}}
+ </h4>
+ <p class="state-description">
+ {{currentState.description}}
+ <a v-if="showButtonDescription" :href="settingsPath">
+ Prometheus server
+ </a>
+ </p>
+ <div class="state-button">
+ <a class="btn btn-success" :href="buttonPath">
+ {{currentState.buttonText}}
+ </a>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 6b3e341f936..5aa3865f96a 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -3,16 +3,14 @@
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
- import GraphPath from './graph_path.vue';
+ import GraphPath from './graph/path.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
- import { timeScaleFormat } from '../utils/date_time_formatters';
+ import { timeScaleFormat, bisectDate } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints';
- const bisectDate = d3.bisector(d => d.time).left;
-
export default {
props: {
graphData: {
@@ -27,6 +25,11 @@
type: Array,
required: true,
},
+ hoverData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
mixins: [MonitoringMixin],
@@ -52,6 +55,7 @@
currentXCoordinate: 0,
currentFlagPosition: 0,
showFlag: false,
+ showFlagContent: false,
showDeployInfo: true,
timeSeries: [],
};
@@ -65,7 +69,7 @@
},
computed: {
- outterViewBox() {
+ outerViewBox() {
return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
},
@@ -122,36 +126,30 @@
const d1 = firstTimeSeries.values[overlayIndex];
if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
- this.currentData = evalTime ? d1 : d0;
- this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
- this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(this.currentData.time));
+ const hoveredDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
+ const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
const currentDeployXPos = this.mouseOverDeployInfo(point.x);
- if (this.currentXCoordinate > (this.graphWidth - 200)) {
- this.currentFlagPosition = this.currentXCoordinate - 103;
- } else {
- this.currentFlagPosition = this.currentXCoordinate;
- }
-
- if (currentDeployXPos) {
- this.showFlag = false;
- } else {
- this.showFlag = true;
- }
+ eventHub.$emit('hoverChanged', {
+ hoveredDate,
+ currentDeployXPos,
+ });
},
renderAxesPaths() {
- this.timeSeries = createTimeSeries(this.graphData.queries[0],
- this.graphWidth,
- this.graphHeight,
- this.graphHeightOffset);
+ this.timeSeries = createTimeSeries(
+ this.graphData.queries[0],
+ this.graphWidth,
+ this.graphHeight,
+ this.graphHeightOffset,
+ );
if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
const axisXScale = d3.time.scale()
- .range([0, this.graphWidth]);
+ .range([0, this.graphWidth - 70]);
const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
@@ -194,6 +192,10 @@
eventHub.$emit('toggleAspectRatio');
}
},
+
+ hoverData() {
+ this.positionFlag();
+ },
},
mounted() {
@@ -203,7 +205,10 @@
</script>
<template>
- <div class="prometheus-graph">
+ <div
+ class="prometheus-graph"
+ @mouseover="showFlagContent = true"
+ @mouseleave="showFlagContent = false">
<h5 class="text-center graph-title">
{{graphData.title}}
</h5>
@@ -211,7 +216,7 @@
class="prometheus-svg-container"
:style="paddingBottomRootSvg">
<svg
- :viewBox="outterViewBox"
+ :viewBox="outerViewBox"
ref="baseSvg">
<g
class="x-axis"
@@ -247,6 +252,7 @@
<graph-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
+ :graph-width="graphWidth"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
@@ -257,6 +263,7 @@
:current-flag-position="currentFlagPosition"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
+ :show-flag-content="showFlagContent"
/>
<rect
class="prometheus-graph-overlay"
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
index 3623d2ed946..e3b8be0c7fb 100644
--- a/app/assets/javascripts/monitoring/components/graph/deployment.vue
+++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue
@@ -19,6 +19,10 @@
type: Number,
required: true,
},
+ graphWidth: {
+ type: Number,
+ required: true,
+ },
},
computed: {
@@ -47,6 +51,14 @@
transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
},
+
+ positionFlag(deployment) {
+ let xPosition = 3;
+ if (deployment.xPos > (this.graphWidth - 200)) {
+ xPosition = -97;
+ }
+ return xPosition;
+ },
},
};
</script>
@@ -77,7 +89,7 @@
<svg
v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box"
- x="3"
+ :x="positionFlag(deployment)"
y="0"
width="92"
height="60">
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index a98e3d06c18..10fb7ff6803 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -23,6 +23,10 @@
type: Number,
required: true,
},
+ showFlagContent: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
@@ -57,6 +61,7 @@
transform="translate(-5, 20)">
</line>
<svg
+ v-if="showFlagContent"
class="rect-text-metric"
:x="currentFlagPosition"
y="0">
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index dbc48c63747..85b6d7f4cbe 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -79,7 +79,11 @@
},
formatMetricUsage(series) {
- return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`;
+ const value = series.values[this.currentDataIndex].value;
+ if (isNaN(value)) {
+ return '-';
+ }
+ return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
},
createSeriesString(index, series) {
diff --git a/app/assets/javascripts/monitoring/components/graph_path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 043f1bf66bb..043f1bf66bb 100644
--- a/app/assets/javascripts/monitoring/components/graph_path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
index 345a0b37a76..31f38aca5d6 100644
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -1,3 +1,5 @@
+import { bisectDate } from '../utils/date_time_formatters';
+
const mixins = {
methods: {
mouseOverDeployInfo(mouseXPos) {
@@ -18,6 +20,7 @@ const mixins = {
return dataFound;
},
+
formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
@@ -40,6 +43,25 @@ const mixins = {
return deploymentDataArray;
}, []);
},
+
+ positionFlag() {
+ const timeSeries = this.timeSeries[0];
+ const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
+ this.currentData = timeSeries.values[hoveredDataIndex];
+ this.currentDataIndex = hoveredDataIndex;
+ this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
+ if (this.currentXCoordinate > (this.graphWidth - 200)) {
+ this.currentFlagPosition = this.currentXCoordinate - 103;
+ } else {
+ this.currentFlagPosition = this.currentXCoordinate;
+ }
+
+ if (this.hoverData.currentDeployXPos) {
+ this.showFlag = false;
+ } else {
+ this.showFlag = true;
+ }
+ },
},
};
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index ef280e02092..104432ef5de 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -3,8 +3,5 @@ import Dashboard from './components/dashboard.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#prometheus-graphs',
- components: {
- Dashboard,
- },
- render: createElement => createElement('dashboard'),
+ render: createElement => createElement(Dashboard),
}));
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 7592af5878e..854636e9a89 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -13,7 +13,7 @@ function normalizeMetrics(metrics) {
...result,
values: result.values.map(([timestamp, value]) => ({
time: new Date(timestamp * 1000),
- value,
+ value: Number(value),
})),
})),
})),
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
index 26bcaa02511..c4c6b1ac1f5 100644
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
@@ -2,6 +2,7 @@ import d3 from 'd3';
export const dateFormat = d3.time.format('%b %-d, %Y');
export const timeFormat = d3.time.format('%-I:%M%p');
+export const bisectDate = d3.bisector(d => d.time).left;
export const timeScaleFormat = d3.time.format.multi([
['.%L', d => d.getMilliseconds()],
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 3cbe06d8fd6..65eec0d8d02 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -56,12 +56,16 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
timeSeriesScaleX.ticks(d3.time.minute, 60);
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
+ const defined = d => !isNaN(d.value) && d.value != null;
+
const lineFunction = d3.svg.line()
+ .defined(defined)
.interpolate('linear')
.x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area()
+ .defined(defined)
.interpolate('linear')
.x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset)
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 93aa29454a0..cf7322ba1da 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -5,7 +5,6 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign,
no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,
brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,
newline-per-chained-call, no-useless-escape, class-methods-use-this */
-/* global Flash */
/* global Autosave */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
@@ -18,12 +17,14 @@ import Dropzone from 'dropzone';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
+import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
import loadAwardsHandler from './awards_handler';
import './autosave';
import './dropzone_input';
import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
+import imageDiffHelper from './image_diff/helpers/index';
window.autosize = autosize;
window.Dropzone = Dropzone;
@@ -42,6 +43,7 @@ export default class Notes {
this.visibilityChange = this.visibilityChange.bind(this);
this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this);
this.onAddDiffNote = this.onAddDiffNote.bind(this);
+ this.onAddImageDiffNote = this.onAddImageDiffNote.bind(this);
this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this);
this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this);
this.removeNote = this.removeNote.bind(this);
@@ -114,6 +116,8 @@ export default class Notes {
$(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
$(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote);
+ // add diff note for images
+ $(document).on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
// hide diff note form
$(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list
@@ -140,6 +144,7 @@ export default class Notes {
$(document).off('click', '.js-note-attachment-delete');
$(document).off('click', '.js-discussion-reply-button');
$(document).off('click', '.js-add-diff-note-button');
+ $(document).off('click', '.js-add-image-diff-note-button');
$(document).off('visibilitychange');
$(document).off('keyup input', '.js-note-text');
$(document).off('click', '.js-note-target-reopen');
@@ -349,7 +354,7 @@ export default class Notes {
Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
- this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
+ this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0));
this.refresh();
}
return;
@@ -412,6 +417,11 @@ export default class Notes {
this.note_ids.push(noteEntity.id);
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row = form.closest('tr');
+
+ if (noteEntity.on_image) {
+ row = form;
+ }
+
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
@@ -423,7 +433,7 @@ export default class Notes {
if (noteEntity.diff_discussion_html) {
var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
- if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
+ if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
// insert the note and the reply button after the temp row
row.after($discussion);
} else {
@@ -449,6 +459,7 @@ export default class Notes {
if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
gl.diffNotesCompileComponents();
+
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
}
@@ -561,7 +572,7 @@ export default class Notes {
form.find('#note_line_code').val(),
// DiffNote
- form.find('#note_position').val()
+ form.find('#note_position').val(),
];
return new Autosave(textarea, key);
}
@@ -582,7 +593,7 @@ export default class Notes {
} else if ($form.hasClass('js-discussion-note-form')) {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
- return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline);
+ return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline.get(0));
}
updateNoteError($parentTimeline) {
@@ -783,9 +794,22 @@ export default class Notes {
$(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
// The notes tr can contain multiple lists of notes, like on the parallel diff
- if (notesTr.find('.discussion-notes').length > 1) {
+ // notesTr does not exist for image diffs
+ if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
+ const $diffFile = $notes.closest('.diff-file');
+ if ($diffFile.length > 0) {
+ const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
+ detail: {
+ // badgeNumber's start with 1 and index starts with 0
+ badgeNumber: $notes.index() + 1,
+ },
+ });
+
+ $diffFile[0].dispatchEvent(removeBadgeEvent);
+ }
+
$notes.remove();
- } else {
+ } else if (notesTr.length > 0) {
notesTr.remove();
}
}
@@ -841,7 +865,11 @@ export default class Notes {
*/
setupDiscussionNoteForm(dataHolder, form) {
// setup note target
- const diffFileData = dataHolder.closest('.text-file');
+ let diffFileData = dataHolder.closest('.text-file');
+
+ if (diffFileData.length === 0) {
+ diffFileData = dataHolder.closest('.image');
+ }
var discussionID = dataHolder.data('discussionId');
@@ -907,6 +935,31 @@ export default class Notes {
});
}
+ onAddImageDiffNote(e) {
+ const $link = $(e.currentTarget || e.target);
+ const $diffFile = $link.closest('.diff-file');
+
+ const clickEvent = new CustomEvent('click.imageDiff', {
+ detail: e,
+ });
+
+ $diffFile[0].dispatchEvent(clickEvent);
+
+ // Setup comment form
+ let newForm;
+ const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
+ const $form = $noteContainer.find('> .discussion-form');
+
+ if ($form.length === 0) {
+ newForm = this.cleanForm(this.formClone.clone());
+ newForm.appendTo($noteContainer);
+ } else {
+ newForm = $form;
+ }
+
+ this.setupDiscussionNoteForm($link, newForm);
+ }
+
toggleDiffNote({
target,
lineType,
@@ -999,10 +1052,25 @@ export default class Notes {
}
cancelDiscussionForm(e) {
- var form;
e.preventDefault();
- form = $(e.target).closest('.js-discussion-note-form');
- return this.removeDiscussionNoteForm(form);
+ const $form = $(e.target).closest('.js-discussion-note-form');
+ const $discussionNote = $(e.target).closest('.discussion-notes');
+
+ if ($discussionNote.length === 0) {
+ // Only send blur event when the discussion form
+ // is not part of a discussion note
+ const $diffFile = $form.closest('.diff-file');
+
+ if ($diffFile.length > 0) {
+ const blurEvent = new CustomEvent('blur.imageDiff', {
+ detail: e,
+ });
+
+ $diffFile[0].dispatchEvent(blurEvent);
+ }
+ }
+
+ return this.removeDiscussionNoteForm($form);
}
/**
@@ -1145,13 +1213,13 @@ export default class Notes {
}
addFlash(...flashParams) {
- this.flashInstance = new Flash(...flashParams);
+ this.flashContainer = new Flash(...flashParams);
}
clearFlash() {
- if (this.flashInstance && this.flashInstance.flashContainer) {
- this.flashInstance.flashContainer.hide();
- this.flashInstance = null;
+ if (this.flashContainer) {
+ this.flashContainer.style.display = 'none';
+ this.flashContainer = null;
}
}
@@ -1414,6 +1482,15 @@ export default class Notes {
// Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove();
+ const $diffFile = $form.closest('.diff-file');
+ if ($diffFile.length > 0) {
+ const blurEvent = new CustomEvent('blur.imageDiff', {
+ detail: e,
+ });
+
+ $diffFile[0].dispatchEvent(blurEvent);
+ }
+
// Reset cached commands list when command is applied
if (hasQuickActions) {
$form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
@@ -1436,7 +1513,28 @@ export default class Notes {
}
// Show final note element on UI
- this.addDiscussionNote($form, note, $notesContainer.length === 0);
+ const isNewDiffComment = $notesContainer.length === 0;
+ this.addDiscussionNote($form, note, isNewDiffComment);
+
+ if (isNewDiffComment) {
+ // Add image badge, avatar badge and toggle discussion badge for new image diffs
+ const notePosition = $form.find('#note_position').val();
+ if ($diffFile.length > 0 && notePosition.length > 0) {
+ const { x, y, width, height } = JSON.parse(notePosition);
+ const addBadgeEvent = new CustomEvent('addBadge.imageDiff', {
+ detail: {
+ x,
+ y,
+ width,
+ height,
+ noteId: `note_${note.id}`,
+ discussionId: note.discussion_id,
+ },
+ });
+
+ $diffFile[0].dispatchEvent(addBadgeEvent);
+ }
+ }
// append flash-container to the Notes list
if ($notesContainer.length) {
@@ -1457,6 +1555,16 @@ export default class Notes {
// Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove();
+ const blurEvent = new CustomEvent('blur.imageDiff', {
+ detail: e,
+ });
+
+ const closestDiffFile = $form.closest('.diff-file');
+
+ if (closestDiffFile.length) {
+ closestDiffFile[0].dispatchEvent(blurEvent);
+ }
+
if (hasQuickActions) {
$notesContainer.find(`#${systemNoteUniqueId}`).remove();
}
@@ -1500,6 +1608,8 @@ export default class Notes {
const $noteBody = $editingNote.find('.js-task-list-container');
const $noteBodyText = $noteBody.find('.note-text');
const { formData, formContent, formAction } = this.getFormData($form);
+ const $diffFile = $form.closest('.diff-file');
+ const $notesContainer = $form.closest('.notes');
// Cache original comment content
const cachedNoteBodyText = $noteBodyText.html();
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index 1a7da84a424..2ce52e4538a 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -1,16 +1,19 @@
<script>
- /* global Flash, Autosave */
+ /* global Autosave */
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import autosize from 'vendor/autosize';
+ import Flash from '../../flash';
import '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
- import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue';
+ import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
+ import issueDiscussionLockedWidget from './issue_discussion_locked_widget.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+ import issuableStateMixin from '../mixins/issuable_state';
export default {
name: 'issueCommentForm',
@@ -26,8 +29,9 @@
};
},
components: {
- confidentialIssue,
+ issueWarning,
issueNoteSignedOutWidget,
+ issueDiscussionLockedWidget,
markdownField,
userAvatarLink,
},
@@ -55,6 +59,9 @@
isIssueOpen() {
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
},
+ canCreateNote() {
+ return this.getIssueData.current_user.can_create_note;
+ },
issueActionButtonTitle() {
if (this.note.length) {
const actionText = this.isIssueOpen ? 'close' : 'reopen';
@@ -90,9 +97,6 @@
endpoint() {
return this.getIssueData.create_note_path;
},
- isConfidentialIssue() {
- return this.getIssueData.confidential;
- },
},
methods: {
...mapActions([
@@ -142,7 +146,7 @@
Flash(
'Something went wrong while adding your comment. Please try again.',
'alert',
- $(this.$refs.commentForm),
+ this.$refs.commentForm,
);
}
} else {
@@ -157,7 +161,7 @@
this.isSubmitting = false;
this.discard(false);
const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
- Flash(msg, 'alert', $(this.$el));
+ Flash(msg, 'alert', this.$el);
this.note = noteData.data.note.note; // Restore textarea content.
this.removePlaceholderNotes();
});
@@ -220,6 +224,9 @@
});
},
},
+ mixins: [
+ issuableStateMixin,
+ ],
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
@@ -235,6 +242,7 @@
<template>
<div>
<issue-note-signed-out-widget v-if="!isLoggedIn" />
+ <issue-discussion-locked-widget v-else-if="!canCreateNote" />
<ul
v-else
class="notes notes-form timeline">
@@ -253,15 +261,22 @@
<div class="timeline-content timeline-content-form">
<form
ref="commentForm"
- class="new-note js-quick-submit common-note-form gfm-form js-main-target-form">
- <confidentialIssue v-if="isConfidentialIssue" />
+ class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"
+ >
+
<div class="error-alert"></div>
+
+ <issue-warning
+ v-if="hasWarning(getIssueData)"
+ :is-locked="isLocked(getIssueData)"
+ :is-confidential="isConfidential(getIssueData)"
+ />
+
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false"
- :is-confidential-issue="isConfidentialIssue"
ref="markdownField">
<textarea
id="note-body"
diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue
index b131ef4b182..baf43190d9e 100644
--- a/app/assets/javascripts/notes/components/issue_discussion.vue
+++ b/app/assets/javascripts/notes/components/issue_discussion.vue
@@ -1,6 +1,6 @@
<script>
- /* global Flash */
import { mapActions, mapGetters } from 'vuex';
+ import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import issueNote from './issue_note.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -133,7 +133,7 @@
this.isReplying = true;
this.$nextTick(() => {
const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
- Flash(msg, 'alert', $(this.$el));
+ Flash(msg, 'alert', this.$el);
this.$refs.noteForm.note = noteText;
callback(err);
});
diff --git a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
new file mode 100644
index 00000000000..e73ec2aaf71
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
@@ -0,0 +1,19 @@
+<script>
+ export default {
+ computed: {
+ lockIcon() {
+ return gl.utils.spriteIcon('lock');
+ },
+ },
+ };
+
+</script>
+
+<template>
+ <div class="disabled-comment text-center">
+ <span class="issuable-note-warning">
+ <span class="icon" v-html="lockIcon"></span>
+ <span>This issue is locked. Only <b>project members</b> can comment.</span>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue
index 1f43b8a16ad..0ddbd672bed 100644
--- a/app/assets/javascripts/notes/components/issue_note.vue
+++ b/app/assets/javascripts/notes/components/issue_note.vue
@@ -1,7 +1,6 @@
<script>
- /* global Flash */
-
import { mapGetters, mapActions } from 'vuex';
+ import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import issueNoteHeader from './issue_note_header.vue';
import issueNoteActions from './issue_note_actions.vue';
@@ -101,7 +100,7 @@
this.isEditing = true;
this.$nextTick(() => {
const msg = 'Something went wrong while editing your comment. Please try again.';
- Flash(msg, 'alert', $(this.$el));
+ Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
});
diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue
index d42e61e3899..c3a340139e7 100644
--- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue
@@ -1,10 +1,9 @@
<script>
- /* global Flash */
-
import { mapActions, mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
+ import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue
index 626c0f2ce18..e2539d6b89d 100644
--- a/app/assets/javascripts/notes/components/issue_note_form.vue
+++ b/app/assets/javascripts/notes/components/issue_note_form.vue
@@ -1,8 +1,9 @@
<script>
import { mapGetters } from 'vuex';
import eventHub from '../event_hub';
- import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue';
+ import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
+ import issuableStateMixin from '../mixins/issuable_state';
export default {
name: 'issueNoteForm',
@@ -39,12 +40,13 @@
};
},
components: {
- confidentialIssue,
+ issueWarning,
markdownField,
},
computed: {
...mapGetters([
'getDiscussionLastNote',
+ 'getIssueData',
'getIssueDataByProp',
'getNotesDataByProp',
'getUserDataByProp',
@@ -67,9 +69,6 @@
isDisabled() {
return !this.note.length || this.isSubmitting;
},
- isConfidentialIssue() {
- return this.getIssueDataByProp('confidential');
- },
},
methods: {
handleUpdate() {
@@ -95,6 +94,9 @@
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note);
},
},
+ mixins: [
+ issuableStateMixin,
+ ],
mounted() {
this.$refs.textarea.focus();
},
@@ -125,7 +127,13 @@
<div class="flash-container timeline-content"></div>
<form
class="edit-note common-note-form js-quick-submit gfm-form">
- <confidentialIssue v-if="isConfidentialIssue" />
+
+ <issue-warning
+ v-if="hasWarning(getIssueData)"
+ :is-locked="isLocked(getIssueData)"
+ :is-confidential="isConfidential(getIssueData)"
+ />
+
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue
index b6fc5e5036f..aecd1f957e5 100644
--- a/app/assets/javascripts/notes/components/issue_notes_app.vue
+++ b/app/assets/javascripts/notes/components/issue_notes_app.vue
@@ -1,6 +1,6 @@
<script>
- /* global Flash */
import { mapGetters, mapActions } from 'vuex';
+ import Flash from '../../flash';
import store from '../stores/';
import * as constants from '../constants';
import issueNote from './issue_note.vue';
diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js
new file mode 100644
index 00000000000..97f3ea0d5de
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/issuable_state.js
@@ -0,0 +1,15 @@
+export default {
+ methods: {
+ isConfidential(issue) {
+ return !!issue.confidential;
+ },
+
+ isLocked(issue) {
+ return !!issue.discussion_locked;
+ },
+
+ hasWarning(issue) {
+ return this.isConfidential(issue) || this.isLocked(issue);
+ },
+ },
+};
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 1a791039909..6f04aecc9b7 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,5 +1,5 @@
-/* global Flash */
import Visibility from 'visibilityjs';
+import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
import * as utils from './utils';
@@ -99,7 +99,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
eTagPoll.makeRequest();
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
- Flash('Commands applied', 'notice', $(noteData.flashContainer));
+ Flash('Commands applied', 'notice', noteData.flashContainer);
}
if (commandsChanges) {
@@ -114,8 +114,8 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
.catch(() => {
Flash(
'Something went wrong while adding your award. Please try again.',
- null,
- $(noteData.flashContainer),
+ 'alert',
+ noteData.flashContainer,
);
});
}
@@ -126,7 +126,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}
if (errors && errors.commands_only) {
- Flash(errors.commands_only, 'notice', $(noteData.flashContainer));
+ Flash(errors.commands_only, 'notice', noteData.flashContainer);
}
commit(types.REMOVE_PLACEHOLDER_NOTES);
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index 838356133cd..f90ac2d9f71 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,5 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */
-/* global Flash */
+import Flash from './flash';
(function() {
this.NotificationsDropdown = (function() {
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 76b97af39f1..9da0aac50a1 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -72,6 +72,13 @@
:title="pipeline.yaml_errors">
yaml invalid
</span>
+ <span
+ v-if="pipeline.flags.failure_reason"
+ v-tooltip
+ class="js-pipeline-url-failure label label-danger"
+ :title="pipeline.failure_reason">
+ error
+ </span>
<a
v-if="pipeline.flags.auto_devops"
tabindex="0"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index c4c63a52358..f3c0aca17ba 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -1,6 +1,4 @@
<script>
- /* global Flash */
- import '~/flash';
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index a4a27247406..1a7a5c2a415 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -13,7 +13,7 @@
* 4. Commit widget
*/
-/* global Flash */
+import Flash from '../../flash';
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index e97f5632dc8..50bdf80c3e3 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,6 +1,5 @@
-/* global Flash */
-import '~/flash';
import Visibility from 'visibilityjs';
+import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
import emptyState from '../components/empty_state.vue';
import errorState from '../components/error_state.vue';
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index bfc416da50b..206023d4ddb 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,6 +1,5 @@
-/* global Flash */
-
import Vue from 'vue';
+import Flash from '../flash';
import PipelinesMediator from './pipeline_details_mediatior';
import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
index 385e7430a7d..823ccd849f4 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
@@ -1,6 +1,5 @@
-/* global Flash */
-
import Visibility from 'visibilityjs';
+import Flash from '../flash';
import Poll from '../lib/utils/poll';
import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
new file mode 100644
index 00000000000..b2b34cb83e1
--- /dev/null
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -0,0 +1,146 @@
+<script>
+ import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
+ import { __, s__, sprintf } from '../../../locale';
+ import csrf from '../../../lib/utils/csrf';
+
+ export default {
+ props: {
+ actionUrl: {
+ type: String,
+ required: true,
+ },
+ confirmWithPassword: {
+ type: Boolean,
+ required: true,
+ },
+ username: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ enteredPassword: '',
+ enteredUsername: '',
+ isOpen: false,
+ };
+ },
+ components: {
+ popupDialog,
+ },
+ computed: {
+ csrfToken() {
+ return csrf.token;
+ },
+ inputLabel() {
+ let confirmationValue;
+ if (this.confirmWithPassword) {
+ confirmationValue = __('password');
+ } else {
+ confirmationValue = __('username');
+ }
+
+ confirmationValue = `<code>${confirmationValue}</code>`;
+
+ return sprintf(
+ s__('Profiles|Type your %{confirmationValue} to confirm:'),
+ { confirmationValue },
+ false,
+ );
+ },
+ text() {
+ return sprintf(
+ s__(`Profiles|
+You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
+Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
+ {
+ yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
+ deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ canSubmit() {
+ if (this.confirmWithPassword) {
+ return this.enteredPassword !== '';
+ }
+
+ return this.enteredUsername === this.username;
+ },
+ onSubmit(status) {
+ if (status) {
+ if (!this.canSubmit()) {
+ return;
+ }
+
+ this.$refs.form.submit();
+ }
+
+ this.toggleOpen(false);
+ },
+ toggleOpen(isOpen) {
+ this.isOpen = isOpen;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div>
+ <popup-dialog
+ v-if="isOpen"
+ :title="s__('Profiles|Delete your account?')"
+ :text="text"
+ :kind="`danger ${!canSubmit() && 'disabled'}`"
+ :primary-button-label="s__('Profiles|Delete account')"
+ @toggle="toggleOpen"
+ @submit="onSubmit">
+
+ <template slot="body" scope="props">
+ <p v-html="props.text"></p>
+
+ <form
+ ref="form"
+ :action="actionUrl"
+ method="post">
+
+ <input
+ type="hidden"
+ name="_method"
+ value="delete" />
+ <input
+ type="hidden"
+ name="authenticity_token"
+ :value="csrfToken" />
+
+ <p id="input-label" v-html="inputLabel"></p>
+
+ <input
+ v-if="confirmWithPassword"
+ name="password"
+ class="form-control"
+ type="password"
+ v-model="enteredPassword"
+ aria-labelledby="input-label" />
+ <input
+ v-else
+ name="username"
+ class="form-control"
+ type="text"
+ v-model="enteredUsername"
+ aria-labelledby="input-label" />
+ </form>
+ </template>
+
+ </popup-dialog>
+
+ <button
+ type="button"
+ class="btn btn-danger"
+ @click="toggleOpen(true)">
+ {{ s__('Profiles|Delete account') }}
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
new file mode 100644
index 00000000000..635056e0eeb
--- /dev/null
+++ b/app/assets/javascripts/profile/account/index.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+
+import deleteAccountModal from './components/delete_account_modal.vue';
+
+const deleteAccountModalEl = document.getElementById('delete-account-modal');
+// eslint-disable-next-line no-new
+new Vue({
+ el: deleteAccountModalEl,
+ components: {
+ deleteAccountModal,
+ },
+ render(createElement) {
+ return createElement('delete-account-modal', {
+ props: {
+ actionUrl: deleteAccountModalEl.dataset.actionUrl,
+ confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword,
+ username: deleteAccountModalEl.dataset.username,
+ },
+ });
+ },
+});
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 3deb242bc1f..0dc02f012e4 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,5 +1,5 @@
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
-/* global Flash */
+import Flash from '../flash';
import { getPagePath } from '../lib/utils/common_utils';
((global) => {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 10da3783123..0a9fdb074e5 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,15 +1,22 @@
+import _ from 'underscore';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
import ProtectedBranchDropdown from './protected_branch_dropdown';
+import AccessorUtilities from '../lib/utils/accessor';
+
+const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults';
export default class ProtectedBranchCreate {
constructor() {
this.$form = $('.js-new-protected-branch');
+ this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+ this.currentProjectUserDefaults = {};
this.buildDropdowns();
}
buildDropdowns() {
const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
+ const $protectedBranchDropdown = this.$form.find('.js-protected-branch-select');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
@@ -28,15 +35,13 @@ export default class ProtectedBranchCreate {
onSelect: this.onSelectCallback,
});
- // Select default
- $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0);
- $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0);
-
// Protected branch dropdown
this.protectedBranchDropdown = new ProtectedBranchDropdown({
- $dropdown: this.$form.find('.js-protected-branch-select'),
+ $dropdown: $protectedBranchDropdown,
onSelect: this.onSelectCallback,
});
+
+ this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown'));
}
// This will run after clicked callback
@@ -45,7 +50,41 @@ export default class ProtectedBranchCreate {
const $branchInput = this.$form.find('input[name="protected_branch[name]"]');
const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
+ const completedForm = !(
+ $branchInput.val() &&
+ $allowedToMergeInput.length &&
+ $allowedToPushInput.length
+ );
+
+ this.savePreviousSelection($allowedToMergeInput.val(), $allowedToPushInput.val());
+ this.$form.find('input[type="submit"]').attr('disabled', completedForm);
+ }
+
+ loadPreviousSelection(mergeDropdown, pushDropdown) {
+ let mergeIndex = 0;
+ let pushIndex = 0;
+ if (this.isLocalStorageAvailable) {
+ const savedDefaults = JSON.parse(window.localStorage.getItem(PB_LOCAL_STORAGE_KEY));
+ if (savedDefaults != null) {
+ mergeIndex = _.findLastIndex(mergeDropdown.fullData.roles, {
+ id: parseInt(savedDefaults.mergeSelection, 0),
+ });
+ pushIndex = _.findLastIndex(pushDropdown.fullData.roles, {
+ id: parseInt(savedDefaults.pushSelection, 0),
+ });
+ }
+ }
+ mergeDropdown.selectRowAtIndex(mergeIndex);
+ pushDropdown.selectRowAtIndex(pushIndex);
+ }
- this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
+ savePreviousSelection(mergeSelection, pushSelection) {
+ if (this.isLocalStorageAvailable) {
+ const branchDefaults = {
+ mergeSelection,
+ pushSelection,
+ };
+ window.localStorage.setItem(PB_LOCAL_STORAGE_KEY, JSON.stringify(branchDefaults));
+ }
}
}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 3b920942a3f..632625da8e7 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -1,6 +1,5 @@
/* eslint-disable no-new */
-/* global Flash */
-
+import Flash from '../flash';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
export default class ProtectedBranchEdit {
@@ -57,7 +56,7 @@ export default class ProtectedBranchEdit {
},
},
error() {
- new Flash('Failed to update branch!', null, $('.js-protected-branches-list'));
+ new Flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list'));
},
}).always(() => {
this.$allowedToMergeDropdown.enable();
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index 09a387c0f9e..dad0ad25b65 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -1,6 +1,5 @@
/* eslint-disable no-new */
-/* global Flash */
-
+import Flash from '../flash';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
@@ -43,7 +42,7 @@ export default class ProtectedTagEdit {
},
},
error() {
- new Flash('Failed to update tag!', null, $('.js-protected-tags-list'));
+ new Flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
},
}).always(() => {
this.$allowedToCreateDropdownButton.enable();
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
index d6c864cb976..cc60aa5939c 100644
--- a/app/assets/javascripts/repo/components/repo.vue
+++ b/app/assets/javascripts/repo/components/repo.vue
@@ -62,7 +62,7 @@ export default {
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
- :body="__('Are you sure you want to discard your changes?')"
+ :text="__('Are you sure you want to discard your changes?')"
@toggle="toggleDialogOpen"
@submit="dialogSubmitted"
/>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
index 119e38c583d..6d8cc964eb2 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -1,5 +1,5 @@
<script>
-/* global Flash */
+import Flash from '../../flash';
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service';
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
index 96d6a75bb61..02d9c775046 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -63,12 +63,7 @@ const RepoEditor = {
const lineNumber = e.target.position.lineNumber;
if (e.target.element.classList.contains('line-numbers')) {
location.hash = `L${lineNumber}`;
- Store.activeLine = lineNumber;
-
- Helper.monacoInstance.setPosition({
- lineNumber: this.activeLine,
- column: 1,
- });
+ Store.setActiveLine(lineNumber);
}
},
},
@@ -101,6 +96,15 @@ const RepoEditor = {
this.setupEditor();
}
},
+
+ activeLine() {
+ if (Helper.monacoInstance) {
+ Helper.monacoInstance.setPosition({
+ lineNumber: this.activeLine,
+ column: 1,
+ });
+ }
+ },
},
computed: {
shouldHideEditor() {
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue
index 2fe369a4693..a87bef6084a 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/repo/components/repo_preview.vue
@@ -14,6 +14,11 @@ export default {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
+ highlightLine() {
+ if (Store.activeLine > -1) {
+ this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
+ }
+ },
},
mounted() {
this.highlightFile();
@@ -26,8 +31,12 @@ export default {
html() {
this.$nextTick(() => {
this.highlightFile();
+ this.highlightLine();
});
},
+ activeLine() {
+ this.highlightLine();
+ },
},
};
</script>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
index 1e40814b95f..e0f3c33003a 100644
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -18,22 +18,40 @@ export default {
},
created() {
- this.addPopEventListener();
+ window.addEventListener('popstate', this.checkHistory);
+ },
+ destroyed() {
+ window.removeEventListener('popstate', this.checkHistory);
},
data: () => Store,
methods: {
- addPopEventListener() {
- window.addEventListener('popstate', () => {
- if (location.href.indexOf('#') > -1) return;
- this.linkClicked({
+ checkHistory() {
+ let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
+ if (!selectedFile) {
+ // Maybe it is not in the current tree but in the opened tabs
+ selectedFile = Helper.getFileFromPath(location.pathname);
+ }
+
+ let lineNumber = null;
+ if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2));
+
+ if (selectedFile) {
+ if (selectedFile.url !== this.activeFile.url) {
+ this.fileClicked(selectedFile, lineNumber);
+ } else {
+ Store.setActiveLine(lineNumber);
+ }
+ } else {
+ // Not opened at all lets open new tab
+ this.fileClicked({
url: location.href,
- });
- });
+ }, lineNumber);
+ }
},
- fileClicked(clickedFile) {
+ fileClicked(clickedFile, lineNumber) {
let file = clickedFile;
if (file.loading) return;
file.loading = true;
@@ -41,17 +59,20 @@ export default {
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
+ Store.setActiveLine(lineNumber);
} else {
const openFile = Helper.getFileFromPath(file.url);
if (openFile) {
file.loading = false;
Store.setActiveFiles(openFile);
+ Store.setActiveLine(lineNumber);
} else {
Service.url = file.url;
Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
+ Store.setActiveLine(lineNumber);
})
.catch(Helper.loadingError);
}
@@ -74,8 +95,8 @@ export default {
<thead v-if="!isMini">
<tr>
<th class="name">Name</th>
- <th class="hidden-sm hidden-xs last-commit">Last Commit</th>
- <th class="hidden-xs last-update text-right">Last Update</th>
+ <th class="hidden-sm hidden-xs last-commit">Last commit</th>
+ <th class="hidden-xs last-update text-right">Last update</th>
</tr>
</thead>
<tbody>
diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js
index ac59a2bed23..46204598e1d 100644
--- a/app/assets/javascripts/repo/helpers/repo_helper.js
+++ b/app/assets/javascripts/repo/helpers/repo_helper.js
@@ -1,7 +1,6 @@
-/* global Flash */
import Service from '../services/repo_service';
import Store from '../stores/repo_store';
-import '../../flash';
+import Flash from '../../flash';
const RepoHelper = {
monacoInstance: null,
@@ -254,7 +253,9 @@ const RepoHelper = {
RepoHelper.key = RepoHelper.genKey();
- history.pushState({ key: RepoHelper.key }, '', url);
+ if (document.location.pathname !== url) {
+ history.pushState({ key: RepoHelper.key }, '', url);
+ }
if (title) {
document.title = title;
diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js
index af83497fa39..830685f7e6e 100644
--- a/app/assets/javascripts/repo/services/repo_service.js
+++ b/app/assets/javascripts/repo/services/repo_service.js
@@ -1,4 +1,3 @@
-/* global Flash */
import axios from 'axios';
import Store from '../stores/repo_store';
import Api from '../../api';
diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js
index 9a4fc40bc69..c633f538c1b 100644
--- a/app/assets/javascripts/repo/stores/repo_store.js
+++ b/app/assets/javascripts/repo/stores/repo_store.js
@@ -1,4 +1,3 @@
-/* global Flash */
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
@@ -26,7 +25,7 @@ const RepoStore = {
},
activeFile: Helper.getDefaultActiveFile(),
activeFileIndex: 0,
- activeLine: 0,
+ activeLine: -1,
activeFileLabel: 'Raw',
files: [],
isCommitable: false,
@@ -85,6 +84,7 @@ const RepoStore = {
if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
RepoStore.binary = file.binary;
+ RepoStore.setActiveLine(-1);
},
setFileActivity(file, openedFile, i) {
@@ -101,6 +101,10 @@ const RepoStore = {
RepoStore.activeFileIndex = i;
},
+ setActiveLine(activeLine) {
+ if (!isNaN(activeLine)) RepoStore.activeLine = activeLine;
+ },
+
setActiveToRaw() {
RepoStore.activeFile.raw = false;
// can't get vue to listen to raw for some reason so RepoStore for now.
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index 05caf177aec..07fee53d814 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,5 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
-/* global Flash */
+import Flash from './flash';
import Api from './api';
(function() {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index e754f6c4460..f63b99ba1c5 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -18,23 +18,8 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Mousetrap.bind('f', (e => this.focusFilter(e)));
Mousetrap.bind('p b', this.onTogglePerfBar);
- const $globalDropdownMenu = $('.global-dropdown-menu');
- const $globalDropdownToggle = $('.global-dropdown-toggle');
const findFileURL = document.body.dataset.findFile;
- $('.global-dropdown').on('hide.bs.dropdown', () => {
- $globalDropdownMenu.removeClass('shortcuts');
- });
-
- Mousetrap.bind('n', () => {
- $globalDropdownMenu.toggleClass('shortcuts');
- $globalDropdownToggle.trigger('click');
-
- if (!$globalDropdownMenu.is(':visible')) {
- $globalDropdownToggle.blur();
- }
- });
-
Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
index f83c3b037ed..74c17bc14a2 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
@@ -1,5 +1,4 @@
-/* global Flash */
-
+import Flash from '../../../flash';
import AssigneeTitle from './assignee_title';
import Assignees from './assignees';
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 8e7abdbffef..22a9a34dda3 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,5 +1,5 @@
<script>
-/* global Flash */
+import Flash from '../../../flash';
import editForm from './edit_form.vue';
export default {
@@ -47,9 +47,9 @@ export default {
</script>
<template>
- <div class="block confidentiality">
+ <div class="block issuable-sidebar-item confidentiality">
<div class="sidebar-collapsed-icon">
- <i class="fa" :class="faEye" aria-hidden="true" data-hidden="true"></i>
+ <i class="fa" :class="faEye" aria-hidden="true"></i>
</div>
<div class="title hide-collapsed">
Confidentiality
@@ -62,19 +62,19 @@ export default {
Edit
</a>
</div>
- <div class="value confidential-value hide-collapsed">
+ <div class="value sidebar-item-value hide-collapsed">
<editForm
v-if="edit"
:toggle-form="toggleForm"
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
- <div v-if="!isConfidential" class="no-value confidential-value">
- <i class="fa fa-eye is-not-confidential"></i>
+ <div v-if="!isConfidential" class="no-value sidebar-item-value">
+ <i class="fa fa-eye sidebar-item-icon"></i>
Not confidential
</div>
- <div v-else class="value confidential-value hide-collapsed">
- <i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>
+ <div v-else class="value sidebar-item-value hide-collapsed">
+ <i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i>
This issue is confidential
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index d578b663a54..dd17b5abd46 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -2,9 +2,6 @@
import editFormButtons from './edit_form_buttons.vue';
export default {
- components: {
- editFormButtons,
- },
props: {
isConfidential: {
required: true,
@@ -19,12 +16,16 @@ export default {
type: Function,
},
},
+
+ components: {
+ editFormButtons,
+ },
};
</script>
<template>
<div class="dropdown open">
- <div class="dropdown-menu confidential-warning-message">
+ <div class="dropdown-menu sidebar-item-warning-message">
<div>
<p v-if="!isConfidential">
You are going to turn on the confidentiality. This means that only team members with
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 97af4a3f505..7ed0619ee6b 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -15,7 +15,7 @@ export default {
},
},
computed: {
- onOrOff() {
+ toggleButtonText() {
return this.isConfidential ? 'Turn Off' : 'Turn On';
},
updateConfidentialBool() {
@@ -26,7 +26,7 @@ export default {
</script>
<template>
- <div class="confidential-warning-message-actions">
+ <div class="sidebar-item-warning-message-actions">
<button
type="button"
class="btn btn-default append-right-10"
@@ -39,7 +39,7 @@ export default {
class="btn btn-close"
@click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
>
- {{ onOrOff }}
+ {{ toggleButtonText }}
</button>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
new file mode 100644
index 00000000000..c7a6edc7c70
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -0,0 +1,61 @@
+<script>
+import editFormButtons from './edit_form_buttons.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+
+export default {
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
+ },
+
+ toggleForm: {
+ required: true,
+ type: Function,
+ },
+
+ updateLockedAttribute: {
+ required: true,
+ type: Function,
+ },
+
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
+
+ mixins: [
+ issuableMixin,
+ ],
+
+ components: {
+ editFormButtons,
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown open">
+ <div class="dropdown-menu sidebar-item-warning-message">
+ <p class="text" v-if="isLocked">
+ Unlock this {{ issuableDisplayName(issuableType) }}?
+ <strong>Everyone</strong>
+ will be able to comment.
+ </p>
+
+ <p class="text" v-else>
+ Lock this {{ issuableDisplayName(issuableType) }}?
+ Only
+ <strong>project members</strong>
+ will be able to comment.
+ </p>
+
+ <edit-form-buttons
+ :is-locked="isLocked"
+ :toggle-form="toggleForm"
+ :update-locked-attribute="updateLockedAttribute"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
new file mode 100644
index 00000000000..c3a553a7605
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -0,0 +1,50 @@
+<script>
+export default {
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
+ },
+
+ toggleForm: {
+ required: true,
+ type: Function,
+ },
+
+ updateLockedAttribute: {
+ required: true,
+ type: Function,
+ },
+ },
+
+ computed: {
+ buttonText() {
+ return this.isLocked ? this.__('Unlock') : this.__('Lock');
+ },
+
+ toggleLock() {
+ return !this.isLocked;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="sidebar-item-warning-message-actions">
+ <button
+ type="button"
+ class="btn btn-default append-right-10"
+ @click="toggleForm"
+ >
+ {{ __('Cancel') }}
+ </button>
+
+ <button
+ type="button"
+ class="btn btn-close"
+ @click.prevent="updateLockedAttribute(toggleLock)"
+ >
+ {{ buttonText }}
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
new file mode 100644
index 00000000000..c4b2900e020
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -0,0 +1,120 @@
+<script>
+/* global Flash */
+import editForm from './edit_form.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+
+export default {
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
+ },
+
+ isEditable: {
+ required: true,
+ type: Boolean,
+ },
+
+ mediator: {
+ required: true,
+ type: Object,
+ validator(mediatorObject) {
+ return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
+ },
+ },
+
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
+
+ mixins: [
+ issuableMixin,
+ ],
+
+ components: {
+ editForm,
+ },
+
+ computed: {
+ lockIconClass() {
+ return this.isLocked ? 'fa-lock' : 'fa-unlock';
+ },
+
+ isLockDialogOpen() {
+ return this.mediator.store.isLockDialogOpen;
+ },
+ },
+
+ methods: {
+ toggleForm() {
+ this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
+ },
+
+ updateLockedAttribute(locked) {
+ this.mediator.service.update(this.issuableType, {
+ discussion_locked: locked,
+ })
+ .then(() => location.reload())
+ .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}`)));
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block issuable-sidebar-item lock">
+ <div class="sidebar-collapsed-icon">
+ <i
+ class="fa"
+ :class="lockIconClass"
+ aria-hidden="true"
+ ></i>
+ </div>
+
+ <div class="title hide-collapsed">
+ Lock {{issuableDisplayName(issuableType) }}
+ <button
+ v-if="isEditable"
+ class="pull-right lock-edit btn btn-blank"
+ type="button"
+ @click.prevent="toggleForm"
+ >
+ {{ __('Edit') }}
+ </button>
+ </div>
+
+ <div class="value sidebar-item-value hide-collapsed">
+ <edit-form
+ v-if="isLockDialogOpen"
+ :toggle-form="toggleForm"
+ :is-locked="isLocked"
+ :update-locked-attribute="updateLockedAttribute"
+ :issuable-type="issuableType"
+ />
+
+ <div
+ v-if="isLocked"
+ class="value sidebar-item-value"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-lock sidebar-item-icon is-active"
+ ></i>
+ {{ __('Locked') }}
+ </div>
+
+ <div
+ v-else
+ class="no-value sidebar-item-value hide-collapsed"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-unlock sidebar-item-icon"
+ ></i>
+ {{ __('Unlocked') }}
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 3c9de02407e..977dd83a7ea 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,5 +1,3 @@
-/* global Flash */
-
function isValidProjectId(id) {
return id > 0;
}
@@ -38,7 +36,7 @@ class SidebarMoveIssue {
data: (searchTerm, callback) => {
this.mediator.fetchAutocompleteProjects(searchTerm)
.then(callback)
- .catch(() => new Flash('An error occurred while fetching projects autocomplete.'));
+ .catch(() => new window.Flash('An error occurred while fetching projects autocomplete.'));
},
renderRow: project => `
<li>
@@ -73,7 +71,7 @@ class SidebarMoveIssue {
this.mediator.moveIssue()
.catch(() => {
- Flash('An error occurred while moving the issue.');
+ window.Flash('An error occurred while moving the issue.');
this.$confirmButton
.enable()
.removeClass('is-loading');
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 3d8972050a9..09b9d75c02d 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -1,46 +1,76 @@
import Vue from 'vue';
-import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
-import sidebarAssignees from './components/assignees/sidebar_assignees';
-import confidential from './components/confidential/confidential_issue_sidebar.vue';
+import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
+import SidebarAssignees from './components/assignees/sidebar_assignees';
+import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
+import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
+import Translate from '../vue_shared/translate';
import Mediator from './sidebar_mediator';
+Vue.use(Translate);
+
+function mountConfidentialComponent(mediator) {
+ const el = document.getElementById('js-confidential-entry-point');
+
+ if (!el) return;
+
+ const dataNode = document.getElementById('js-confidential-issue-data');
+ const initialData = JSON.parse(dataNode.innerHTML);
+
+ const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
+
+ new ConfidentialComp({
+ propsData: {
+ isConfidential: initialData.is_confidential,
+ isEditable: initialData.is_editable,
+ service: mediator.service,
+ },
+ }).$mount(el);
+}
+
+function mountLockComponent(mediator) {
+ const el = document.getElementById('js-lock-entry-point');
+
+ if (!el) return;
+
+ const dataNode = document.getElementById('js-lock-issue-data');
+ const initialData = JSON.parse(dataNode.innerHTML);
+
+ const LockComp = Vue.extend(LockIssueSidebar);
+
+ new LockComp({
+ propsData: {
+ isLocked: initialData.is_locked,
+ isEditable: initialData.is_editable,
+ mediator,
+ issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
+ },
+ }).$mount(el);
+}
+
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
mediator.fetch();
- const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
- const confidentialEl = document.querySelector('#js-confidential-entry-point');
+ const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) {
- new Vue(sidebarAssignees).$mount(sidebarAssigneesEl);
+ new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
}
- if (confidentialEl) {
- const dataNode = document.getElementById('js-confidential-issue-data');
- const initialData = JSON.parse(dataNode.innerHTML);
+ mountConfidentialComponent(mediator);
+ mountLockComponent(mediator);
- const ConfidentialComp = Vue.extend(confidential);
-
- new ConfidentialComp({
- propsData: {
- isConfidential: initialData.is_confidential,
- isEditable: initialData.is_editable,
- service: mediator.service,
- },
- }).$mount(confidentialEl);
-
- new SidebarMoveIssue(
- mediator,
- $('.js-move-issue'),
- $('.js-move-issue-confirmation-button'),
- ).init();
- }
+ new SidebarMoveIssue(
+ mediator,
+ $('.js-move-issue'),
+ $('.js-move-issue-confirmation-button'),
+ ).init();
- new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
+ new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 2fe6e5b31f0..ede3a0de144 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,5 +1,4 @@
-/* global Flash */
-
+import Flash from '../flash';
import Service from './services/sidebar_service';
import Store from './stores/sidebar_store';
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index cc04a2a3fcf..d5d04103f3f 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -15,6 +15,7 @@ export default class SidebarStore {
};
this.autocompleteProjects = [];
this.moveToProjectId = 0;
+ this.isLockDialogOpen = false;
SidebarStore.singleton = this;
}
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 4505a79a2df..3f811c59cb9 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import FilesCommentButton from './files_comment_button';
+import imageDiffHelper from './image_diff/helpers/index';
const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
@@ -74,7 +75,11 @@ export default class SingleFileDiff {
gl.diffNotesCompileComponents();
}
- FilesCommentButton.init($(_this.file));
+ const $file = $(_this.file);
+ FilesCommentButton.init($file);
+
+ const canCreateNote = $file.closest('.files').is('[data-can-create-note]');
+ imageDiffHelper.initImageDiff($file[0], canCreateNote);
if (cb) cb();
};
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 3a06b477d7c..77db075d1ef 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
-/* global Flash */
-
+import Flash from './flash';
import { __, s__ } from './locale';
export default class Star {
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index c39f569da5e..dcbec40c79e 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,6 +1,5 @@
-/* global Flash */
-
import 'deckar01-task_list';
+import Flash from './flash';
export default class TaskList {
constructor(options = {}) {
diff --git a/app/assets/javascripts/two_factor_auth.js b/app/assets/javascripts/two_factor_auth.js
index d26f61562a5..e3414d9afff 100644
--- a/app/assets/javascripts/two_factor_auth.js
+++ b/app/assets/javascripts/two_factor_auth.js
@@ -1,4 +1,5 @@
-/* global U2FRegister */
+import U2FRegister from './u2f/register';
+
document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth');
const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true';
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 8821b22477f..a3cc04e35fe 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,118 +1,108 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
+/* eslint-disable func-names, wrap-iife */
/* global u2f */
-/* global U2FError */
-/* global U2FUtil */
-
import _ from 'underscore';
+import isU2FSupported from './util';
+import U2FError from './error';
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> authenticated -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
-(function() {
- const global = window.gl || (window.gl = {});
-
- global.U2FAuthenticate = (function() {
- function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) {
- this.container = container;
- this.renderNotSupported = this.renderNotSupported.bind(this);
- this.renderAuthenticated = this.renderAuthenticated.bind(this);
- this.renderError = this.renderError.bind(this);
- this.renderInProgress = this.renderInProgress.bind(this);
- this.renderTemplate = this.renderTemplate.bind(this);
- this.authenticate = this.authenticate.bind(this);
- this.start = this.start.bind(this);
- this.appId = u2fParams.app_id;
- this.challenge = u2fParams.challenge;
- this.form = form;
- this.fallbackButton = fallbackButton;
- this.fallbackUI = fallbackUI;
- if (this.fallbackButton) this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
- this.signRequests = u2fParams.sign_requests.map(function(request) {
- // The U2F Javascript API v1.1 requires a single challenge, with
- // _no challenges per-request_. The U2F Javascript API v1.0 requires a
- // challenge per-request, which is done by copying the single challenge
- // into every request.
- //
- // In either case, we don't need the per-request challenges that the server
- // has generated, so we can remove them.
- //
- // Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
- // This can be removed once we upgrade.
- // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
- return _(request).omit('challenge');
- });
+export default class U2FAuthenticate {
+ constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
+ this.container = container;
+ this.renderNotSupported = this.renderNotSupported.bind(this);
+ this.renderAuthenticated = this.renderAuthenticated.bind(this);
+ this.renderError = this.renderError.bind(this);
+ this.renderInProgress = this.renderInProgress.bind(this);
+ this.renderTemplate = this.renderTemplate.bind(this);
+ this.authenticate = this.authenticate.bind(this);
+ this.start = this.start.bind(this);
+ this.appId = u2fParams.app_id;
+ this.challenge = u2fParams.challenge;
+ this.form = form;
+ this.fallbackButton = fallbackButton;
+ this.fallbackUI = fallbackUI;
+ if (this.fallbackButton) {
+ this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
}
- U2FAuthenticate.prototype.start = function() {
- if (U2FUtil.isU2FSupported()) {
- return this.renderInProgress();
- } else {
- return this.renderNotSupported();
- }
- };
+ // The U2F Javascript API v1.1 requires a single challenge, with
+ // _no challenges per-request_. The U2F Javascript API v1.0 requires a
+ // challenge per-request, which is done by copying the single challenge
+ // into every request.
+ //
+ // In either case, we don't need the per-request challenges that the server
+ // has generated, so we can remove them.
+ //
+ // Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
+ // This can be removed once we upgrade.
+ // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
+ this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge'));
- U2FAuthenticate.prototype.authenticate = function() {
- return u2f.sign(this.appId, this.challenge, this.signRequests, (function(_this) {
- return function(response) {
- var error;
- if (response.errorCode) {
- error = new U2FError(response.errorCode, 'authenticate');
- return _this.renderError(error);
- } else {
- return _this.renderAuthenticated(JSON.stringify(response));
- }
- };
- })(this), 10);
+ this.templates = {
+ notSupported: '#js-authenticate-u2f-not-supported',
+ setup: '#js-authenticate-u2f-setup',
+ inProgress: '#js-authenticate-u2f-in-progress',
+ error: '#js-authenticate-u2f-error',
+ authenticated: '#js-authenticate-u2f-authenticated',
};
+ }
- // Rendering #
- U2FAuthenticate.prototype.templates = {
- "notSupported": "#js-authenticate-u2f-not-supported",
- "setup": '#js-authenticate-u2f-setup',
- "inProgress": '#js-authenticate-u2f-in-progress',
- "error": '#js-authenticate-u2f-error',
- "authenticated": '#js-authenticate-u2f-authenticated'
- };
+ start() {
+ if (isU2FSupported()) {
+ return this.renderInProgress();
+ }
+ return this.renderNotSupported();
+ }
- U2FAuthenticate.prototype.renderTemplate = function(name, params) {
- var template, templateString;
- templateString = $(this.templates[name]).html();
- template = _.template(templateString);
- return this.container.html(template(params));
- };
+ authenticate() {
+ return u2f.sign(this.appId, this.challenge, this.signRequests, (function (_this) {
+ return function (response) {
+ if (response.errorCode) {
+ const error = new U2FError(response.errorCode, 'authenticate');
+ return _this.renderError(error);
+ }
+ return _this.renderAuthenticated(JSON.stringify(response));
+ };
+ })(this), 10);
+ }
- U2FAuthenticate.prototype.renderInProgress = function() {
- this.renderTemplate('inProgress');
- return this.authenticate();
- };
+ renderTemplate(name, params) {
+ const templateString = $(this.templates[name]).html();
+ const template = _.template(templateString);
+ return this.container.html(template(params));
+ }
- U2FAuthenticate.prototype.renderError = function(error) {
- this.renderTemplate('error', {
- error_message: error.message(),
- error_code: error.errorCode
- });
- return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress);
- };
+ renderInProgress() {
+ this.renderTemplate('inProgress');
+ return this.authenticate();
+ }
- U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) {
- this.renderTemplate('authenticated');
- const container = this.container[0];
- container.querySelector('#js-device-response').value = deviceResponse;
- container.querySelector(this.form).submit();
- this.fallbackButton.classList.add('hidden');
- };
+ renderError(error) {
+ this.renderTemplate('error', {
+ error_message: error.message(),
+ error_code: error.errorCode,
+ });
+ return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress);
+ }
- U2FAuthenticate.prototype.renderNotSupported = function() {
- return this.renderTemplate('notSupported');
- };
+ renderAuthenticated(deviceResponse) {
+ this.renderTemplate('authenticated');
+ const container = this.container[0];
+ container.querySelector('#js-device-response').value = deviceResponse;
+ container.querySelector(this.form).submit();
+ this.fallbackButton.classList.add('hidden');
+ }
- U2FAuthenticate.prototype.switchToFallbackUI = function() {
- this.fallbackButton.classList.add('hidden');
- this.container[0].classList.add('hidden');
- this.fallbackUI.classList.remove('hidden');
- };
+ renderNotSupported() {
+ return this.renderTemplate('notSupported');
+ }
+
+ switchToFallbackUI() {
+ this.fallbackButton.classList.add('hidden');
+ this.container[0].classList.add('hidden');
+ this.fallbackUI.classList.remove('hidden');
+ }
- return U2FAuthenticate;
- })();
-})();
+}
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index 3119b3480c3..1a98564ff55 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,25 +1,22 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, max-len */
-/* global u2f */
+export default class U2FError {
+ constructor(errorCode, u2fFlowType) {
+ this.errorCode = errorCode;
+ this.message = this.message.bind(this);
+ this.httpsDisabled = window.location.protocol !== 'https:';
+ this.u2fFlowType = u2fFlowType;
+ }
-(function() {
- this.U2FError = (function() {
- function U2FError(errorCode, u2fFlowType) {
- this.errorCode = errorCode;
- this.message = this.message.bind(this);
- this.httpsDisabled = window.location.protocol !== 'https:';
- this.u2fFlowType = u2fFlowType;
- }
-
- U2FError.prototype.message = function() {
- if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
- return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
- } else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) {
- if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.';
- if (this.u2fFlowType === 'register') return 'This device has already been registered with us.';
+ message() {
+ if (this.errorCode === window.u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
+ return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
+ } else if (this.errorCode === window.u2f.ErrorCodes.DEVICE_INELIGIBLE) {
+ if (this.u2fFlowType === 'authenticate') {
+ return 'This device has not been registered with us.';
}
- return "There was a problem communicating with your device.";
- };
-
- return U2FError;
- })();
-}).call(window);
+ if (this.u2fFlowType === 'register') {
+ return 'This device has already been registered with us.';
+ }
+ }
+ return 'There was a problem communicating with your device.';
+ }
+}
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 3a2534d553b..cc3f02e75f6 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,98 +1,89 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
+/* eslint-disable func-names, wrap-iife */
/* global u2f */
-/* global U2FError */
-/* global U2FUtil */
import _ from 'underscore';
+import isU2FSupported from './util';
+import U2FError from './error';
// Register U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> registered -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
-(function() {
- this.U2FRegister = (function() {
- function U2FRegister(container, u2fParams) {
- this.container = container;
- this.renderNotSupported = this.renderNotSupported.bind(this);
- this.renderRegistered = this.renderRegistered.bind(this);
- this.renderError = this.renderError.bind(this);
- this.renderInProgress = this.renderInProgress.bind(this);
- this.renderSetup = this.renderSetup.bind(this);
- this.renderTemplate = this.renderTemplate.bind(this);
- this.register = this.register.bind(this);
- this.start = this.start.bind(this);
- this.appId = u2fParams.app_id;
- this.registerRequests = u2fParams.register_requests;
- this.signRequests = u2fParams.sign_requests;
- }
+export default class U2FRegister {
+ constructor(container, u2fParams) {
+ this.container = container;
+ this.renderNotSupported = this.renderNotSupported.bind(this);
+ this.renderRegistered = this.renderRegistered.bind(this);
+ this.renderError = this.renderError.bind(this);
+ this.renderInProgress = this.renderInProgress.bind(this);
+ this.renderSetup = this.renderSetup.bind(this);
+ this.renderTemplate = this.renderTemplate.bind(this);
+ this.register = this.register.bind(this);
+ this.start = this.start.bind(this);
+ this.appId = u2fParams.app_id;
+ this.registerRequests = u2fParams.register_requests;
+ this.signRequests = u2fParams.sign_requests;
- U2FRegister.prototype.start = function() {
- if (U2FUtil.isU2FSupported()) {
- return this.renderSetup();
- } else {
- return this.renderNotSupported();
- }
+ this.templates = {
+ notSupported: '#js-register-u2f-not-supported',
+ setup: '#js-register-u2f-setup',
+ inProgress: '#js-register-u2f-in-progress',
+ error: '#js-register-u2f-error',
+ registered: '#js-register-u2f-registered',
};
+ }
- U2FRegister.prototype.register = function() {
- return u2f.register(this.appId, this.registerRequests, this.signRequests, (function(_this) {
- return function(response) {
- var error;
- if (response.errorCode) {
- error = new U2FError(response.errorCode, 'register');
- return _this.renderError(error);
- } else {
- return _this.renderRegistered(JSON.stringify(response));
- }
- };
- })(this), 10);
- };
+ start() {
+ if (isU2FSupported()) {
+ return this.renderSetup();
+ }
+ return this.renderNotSupported();
+ }
- // Rendering #
- U2FRegister.prototype.templates = {
- "notSupported": "#js-register-u2f-not-supported",
- "setup": '#js-register-u2f-setup',
- "inProgress": '#js-register-u2f-in-progress',
- "error": '#js-register-u2f-error',
- "registered": '#js-register-u2f-registered'
- };
+ register() {
+ return u2f.register(this.appId, this.registerRequests, this.signRequests, (function (_this) {
+ return function (response) {
+ if (response.errorCode) {
+ const error = new U2FError(response.errorCode, 'register');
+ return _this.renderError(error);
+ }
+ return _this.renderRegistered(JSON.stringify(response));
+ };
+ })(this), 10);
+ }
- U2FRegister.prototype.renderTemplate = function(name, params) {
- var template, templateString;
- templateString = $(this.templates[name]).html();
- template = _.template(templateString);
- return this.container.html(template(params));
- };
+ renderTemplate(name, params) {
+ const templateString = $(this.templates[name]).html();
+ const template = _.template(templateString);
+ return this.container.html(template(params));
+ }
- U2FRegister.prototype.renderSetup = function() {
- this.renderTemplate('setup');
- return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
- };
+ renderSetup() {
+ this.renderTemplate('setup');
+ return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
+ }
- U2FRegister.prototype.renderInProgress = function() {
- this.renderTemplate('inProgress');
- return this.register();
- };
+ renderInProgress() {
+ this.renderTemplate('inProgress');
+ return this.register();
+ }
- U2FRegister.prototype.renderError = function(error) {
- this.renderTemplate('error', {
- error_message: error.message(),
- error_code: error.errorCode
- });
- return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
- };
+ renderError(error) {
+ this.renderTemplate('error', {
+ error_message: error.message(),
+ error_code: error.errorCode,
+ });
+ return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
+ }
- U2FRegister.prototype.renderRegistered = function(deviceResponse) {
- this.renderTemplate('registered');
- // Prefer to do this instead of interpolating using Underscore templates
- // because of JSON escaping issues.
- return this.container.find("#js-device-response").val(deviceResponse);
- };
-
- U2FRegister.prototype.renderNotSupported = function() {
- return this.renderTemplate('notSupported');
- };
+ renderRegistered(deviceResponse) {
+ this.renderTemplate('registered');
+ // Prefer to do this instead of interpolating using Underscore templates
+ // because of JSON escaping issues.
+ return this.container.find('#js-device-response').val(deviceResponse);
+ }
- return U2FRegister;
- })();
-}).call(window);
+ renderNotSupported() {
+ return this.renderTemplate('notSupported');
+ }
+}
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index 813d363db00..9771ff935c2 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -1,12 +1,3 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife */
-(function() {
- this.U2FUtil = (function() {
- function U2FUtil() {}
-
- U2FUtil.isU2FSupported = function() {
- return window.u2f;
- };
-
- return U2FUtil;
- })();
-}).call(window);
+export default function isU2FSupported() {
+ return window.u2f;
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
index e98d147733c..e86a0f7e749 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -1,6 +1,5 @@
-/* global Flash */
-
import '~/lib/utils/datetime_utility';
+import Flash from '../../flash';
import MemoryUsage from './mr_widget_memory_usage';
import StatusIcon from './mr_widget_status_icon';
import MRWidgetService from '../services/mr_widget_service';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
index bdfd4d9667c..05c4a28be88 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
@@ -1,4 +1,4 @@
-/* global Flash */
+import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon';
import MRWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
index 74fc52796a0..2dfd87ed904 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
@@ -1,5 +1,4 @@
-/* global Flash */
-
+import Flash from '../../../flash';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import tooltip from '../../../vue_shared/directives/tooltip';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index 0c48a484fe8..b8a96b23012 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -1,7 +1,7 @@
-/* global Flash */
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
+import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub';
@@ -38,24 +38,40 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
- mergeButtonClass() {
- const defaultClass = 'btn btn-sm btn-success accept-merge-request';
- const failedClass = `${defaultClass} btn-danger`;
- const inActionClass = `${defaultClass} btn-info`;
+ status() {
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) {
- return failedClass;
+ return 'failed';
} else if (!pipeline) {
- return defaultClass;
+ return 'success';
} else if (isPipelineActive) {
- return inActionClass;
+ return 'pending';
} else if (isPipelineFailed) {
+ return 'failed';
+ }
+
+ return 'success';
+ },
+ mergeButtonClass() {
+ const defaultClass = 'btn btn-sm btn-success accept-merge-request';
+ const failedClass = `${defaultClass} btn-danger`;
+ const inActionClass = `${defaultClass} btn-info`;
+
+ if (this.status === 'failed') {
return failedClass;
+ } else if (this.status === 'pending') {
+ return inActionClass;
}
return defaultClass;
},
+ iconClass() {
+ if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) {
+ return 'failed';
+ }
+ return 'success';
+ },
mergeButtonText() {
if (this.isMergingImmediately) {
return 'Merge in progress';
@@ -84,13 +100,8 @@ export default {
},
},
methods: {
- isMergeAllowed() {
- return !this.mr.onlyAllowMergeIfPipelineSucceeds ||
- this.mr.isPipelinePassing ||
- this.mr.isPipelineSkipped;
- },
shouldShowMergeControls() {
- return this.isMergeAllowed() || this.shouldShowMergeWhenPipelineSucceedsText;
+ return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
},
updateCommitMessage() {
const cmwd = this.mr.commitMessageWithDescription;
@@ -209,7 +220,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="success" />
+ <status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
index 54be1fbe675..4f83350e07c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -1,4 +1,3 @@
-/* global Flash */
import statusIcon from '../mr_widget_status_icon';
import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
@@ -27,12 +26,12 @@ export default {
.then(res => res.json())
.then((res) => {
eventHub.$emit('UpdateWidgetData', res);
- new Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
+ new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
$('.merge-request .detail-page-description .title').text(this.mr.title);
})
.catch(() => {
this.isMakingRequest = false;
- new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ new window.Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 044b664484b..4f497b204a3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -1,5 +1,4 @@
-/* global Flash */
-
+import Flash from '../flash';
import {
WidgetHeader,
WidgetMergeHelp,
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index e554082149b..c1f7e64f580 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -73,6 +73,7 @@ export default class MergeRequestStore {
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
this.hasSHAChanged = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false;
+ this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
// Cherry-pick and Revert actions related
diff --git a/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue
deleted file mode 100644
index 397d16331d5..00000000000
--- a/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-<script>
- export default {
- name: 'confidentialIssueWarning',
- };
-</script>
-<template>
- <div class="confidential-issue-warning">
- <i
- aria-hidden="true"
- class="fa fa-eye-slash">
- </i>
- <span>
- This is a confidential issue. Your comment will not be visible to the public.
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
new file mode 100644
index 00000000000..16c0a8efcd2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -0,0 +1,55 @@
+<script>
+ export default {
+ props: {
+ isLocked: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+
+ isConfidential: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+
+ computed: {
+ iconClass() {
+ return {
+ 'fa-eye-slash': this.isConfidential,
+ 'fa-lock': this.isLocked,
+ };
+ },
+
+ isLockedAndConfidential() {
+ return this.isConfidential && this.isLocked;
+ },
+ },
+ };
+</script>
+<template>
+ <div class="issuable-note-warning">
+ <i
+ aria-hidden="true"
+ class="fa icon"
+ :class="iconClass"
+ v-if="!isLockedAndConfidential"
+ ></i>
+
+ <span v-if="isLockedAndConfidential">
+ {{ __('This issue is confidential and locked.') }}
+ {{ __('People without permission will never get a notification and won\'t be able to comment.') }}
+ </span>
+
+ <span v-else-if="isConfidential">
+ {{ __('This is a confidential issue.') }}
+ {{ __('Your comment will not be visible to the public.') }}
+ </span>
+
+ <span v-else-if="isLocked">
+ {{ __('This issue is locked.') }}
+ {{ __('Only project members can comment.') }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 759d30c9c7c..af4187fab46 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,5 +1,5 @@
<script>
- /* global Flash */
+ import Flash from '../../../flash';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
index 994b33bc1c9..9279b50cd55 100644
--- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue
+++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
@@ -7,7 +7,7 @@ export default {
type: String,
required: true,
},
- body: {
+ text: {
type: String,
required: true,
},
@@ -63,7 +63,9 @@ export default {
<h4 class="modal-title">{{this.title}}</h4>
</div>
<div class="modal-body">
- <p>{{this.body}}</p>
+ <slot name="body" :text="text">
+ <p>{{text}}</p>
+ </slot>
</div>
<div class="modal-footer">
<button
diff --git a/app/assets/javascripts/vue_shared/mixins/issuable.js b/app/assets/javascripts/vue_shared/mixins/issuable.js
new file mode 100644
index 00000000000..263361587e0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/issuable.js
@@ -0,0 +1,9 @@
+export default {
+ methods: {
+ issuableDisplayName(issuableType) {
+ const displayName = issuableType.replace(/_/, ' ');
+
+ return this.__ ? this.__(displayName) : displayName;
+ },
+ },
+};
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 74b846217bb..c7be94e2c8e 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -30,16 +30,16 @@
@import "framework/media_object";
@import "framework/mobile";
@import "framework/modal";
-@import "framework/nav";
-@import "framework/new-nav";
@import "framework/pagination";
@import "framework/panels";
+@import "framework/secondary-navigation-elements";
@import "framework/selects";
@import "framework/sidebar";
@import "framework/new-sidebar";
@import "framework/tables";
@import "framework/notes";
@import "framework/timeline";
+@import "framework/tooltips";
@import "framework/typography";
@import "framework/zen";
@import "framework/blank";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 667b73e150d..f0e6b23757f 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -115,8 +115,7 @@
@return $unfoldedTransition;
}
-.btn,
-.global-dropdown-toggle {
+.btn {
@include transition(background-color, border-color, color, box-shadow);
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 1d72a70f0f5..dbd990f84c1 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -207,6 +207,16 @@
&.user-cover-block {
padding: 24px 0 0;
+
+ .nav-links {
+ justify-content: center;
+ width: 100%;
+ float: none;
+
+ &.scrolling-tabs {
+ float: none;
+ }
+ }
}
.group-info {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index d178bc17462..b131e2d57ee 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,3 +1,25 @@
+@mixin btn-comment-icon {
+ border-radius: 50%;
+ background: $white-light;
+ padding: 1px 5px;
+ font-size: 12px;
+ color: $blue-500;
+ width: 23px;
+ height: 23px;
+ border: 1px solid $blue-500;
+
+ &:hover,
+ &.inverted {
+ background: $blue-500;
+ border-color: $blue-600;
+ color: $white-light;
+ }
+
+ &:active {
+ outline: 0;
+ }
+}
+
@mixin btn-default {
border-radius: 3px;
font-size: $gl-font-size;
@@ -381,7 +403,11 @@
background: transparent;
border: 0;
+ &:hover,
+ &:active,
&:focus {
outline: 0;
+ background: transparent;
+ box-shadow: none;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index fa92d4ccf4f..5b950ae0ba0 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -749,7 +749,7 @@
margin-bottom: $dropdown-vertical-offset;
}
- li {
+ li:not(.dropdown-bold-header) {
display: block;
padding: 0 1px;
@@ -889,7 +889,7 @@
@include new-style-dropdown('.breadcrumbs-list .dropdown ');
@include new-style-dropdown('.js-namespace-select + ');
-header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu {
+header.header-content .dropdown-menu.projects-dropdown-menu {
padding: 0;
}
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 6b69e8018be..52b87de7a3d 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -5,7 +5,7 @@
@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
// Header
- header.navbar-gitlab-new {
+ .navbar-gitlab {
background-color: $color-900;
.navbar-collapse {
@@ -95,7 +95,7 @@
}
}
- .title {
+ .navbar .title {
> a {
&:hover,
&:focus {
@@ -200,9 +200,9 @@ body {
&.ui_light {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
- header.navbar-gitlab-new {
+ .navbar-gitlab {
background-color: $theme-gray-100;
- box-shadow: 0 2px 0 0 $border-color;
+ box-shadow: 0 1px 0 0 $border-color;
.logo-text svg {
fill: $theme-gray-900;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index d932ea8794f..22945e935ef 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -1,112 +1,37 @@
-/*
- * Application Header
- *
- */
+.content-wrapper.page-with-new-nav {
+ margin-top: $header-height;
+}
-header {
+.navbar-gitlab {
@include new-style-dropdown;
- transition: padding $sidebar-transition-duration;
-
- &.navbar-empty {
- height: $header-height;
- background: $white-light;
- border-bottom: 1px solid $white-normal;
-
- .center-logo {
- margin: 8px 0;
- text-align: center;
-
- .tanuki-logo,
- img {
- height: 36px;
- }
- }
- }
-
&.navbar-gitlab {
padding: 0 16px;
z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
- background-color: $gray-light;
border: none;
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
left: 0;
right: 0;
- color: $gl-text-color-secondary;
border-radius: 0;
- @media (max-width: $screen-xs-min) {
- padding: 0 16px;
- }
-
- &.with-horizontal-nav {
- border-bottom: 0;
+ .logo-text {
+ line-height: initial;
- .navbar-border {
- height: 1px;
- position: absolute;
- right: 0;
- left: 0;
- bottom: -1px;
- background-color: $border-color;
- opacity: 0;
+ svg {
+ width: 55px;
+ height: 14px;
+ margin: 0;
+ fill: $white-light;
}
}
.container-fluid {
- width: 100% !important;
- filter: none;
padding: 0;
- .nav > li > a {
- color: currentColor;
- font-size: 18px;
- padding: 0;
- margin: (($header-height - 28) / 2) 3px;
- margin-left: 8px;
- height: 28px;
- min-width: 32px;
- line-height: 28px;
- text-align: center;
-
- &.header-user-dropdown-toggle {
- margin-left: 14px;
-
- &:hover,
- &:focus,
- &:active {
- .header-user-avatar {
- border-color: rgba($avatar-border, .2);
- }
- }
- }
-
- &:hover,
- &:focus,
- &:active {
- background-color: transparent;
- color: $gl-text-color;
-
- svg {
- fill: $gl-text-color;
- }
- }
-
- .fa-caret-down {
- font-size: 14px;
- }
-
- .fa-chevron-down {
- position: relative;
- top: -3px;
- font-size: 10px;
- }
- }
-
.user-counter {
svg {
margin-right: 3px;
@@ -114,81 +39,117 @@ header {
}
.navbar-toggle {
- color: $nav-toggle-gray;
- margin: 5px 0;
- border-radius: 0;
right: -10px;
- padding: 6px 10px;
+ border-radius: 0;
+ min-width: 45px;
+ padding: 0;
+ margin-right: -7px;
+ font-size: 14px;
+ text-align: center;
+ color: currentColor;
- &:hover {
- background-color: $white-normal;
+ &:hover,
+ &:focus,
+ &.active {
+ color: currentColor;
+ background-color: transparent;
}
- &.active {
- color: $gl-text-color-secondary;
+ .more-icon,
+ .close-icon {
+ fill: $white-light;
+ margin: auto;
}
}
}
}
- &.navbar-gitlab-new {
- .close-icon {
+ .close-icon {
+ display: none;
+ }
+
+ .menu-expanded {
+ .more-icon {
display: none;
}
- .menu-expanded {
- .more-icon {
- display: none;
- }
-
- .close-icon {
- display: block;
- }
+ .close-icon {
+ display: block;
}
}
- .global-dropdown {
- position: absolute;
- left: -10px;
+ .header-content {
+ display: -webkit-flex;
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+ min-height: $header-height;
+ padding-left: 0;
- .badge {
- font-size: 11px;
+ .title-container {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-align-items: stretch;
+ align-items: stretch;
+ -webkit-flex: 1 1 auto;
+ flex: 1 1 auto;
+ padding-top: 0;
+ overflow: visible;
}
- li {
- &.active a {
- font-weight: $gl-font-weight-bold;
+ .title {
+ padding-right: 0;
+ color: currentColor;
+ display: -webkit-flex;
+ display: flex;
+ position: relative;
+ margin: 0;
+ font-size: 18px;
+ vertical-align: top;
+ white-space: nowrap;
+
+ img {
+ height: 28px;
+ margin-right: 8px;
}
- }
- }
- .global-dropdown-toggle {
- margin: 7px 0;
- font-size: 18px;
- padding: 6px 10px;
- border: none;
- background-color: $gray-light;
+ &.wrap {
+ white-space: normal;
+ }
- &:hover {
- background-color: $white-normal;
- }
+ &.initializing {
+ opacity: 0;
+ }
+
+ a {
+ display: -webkit-flex;
+ display: flex;
+ align-items: center;
+ padding: 2px 8px;
+ margin: 5px 2px 5px -8px;
+ border-radius: $border-radius-default;
- &:focus {
- outline: none;
- background-color: $white-normal;
+ svg {
+ @media (min-width: $screen-sm-min) {
+ margin-right: 8px;
+ }
+ }
+ }
+
+ .project-item-select {
+ right: auto;
+ left: 0;
+ }
}
- }
- .header-content {
- display: flex;
- justify-content: space-between;
- position: relative;
- min-height: $header-height;
- padding-left: 30px;
+ .dropdown.open {
+ > a {
+ border-bottom-color: $white-light;
+ }
+ }
&.menu-expanded {
@media (max-width: $screen-xs-max) {
- .header-logo,
.title-container {
display: none;
}
@@ -198,111 +159,180 @@ header {
}
}
}
+ }
- .dropdown-menu {
- margin-top: -5px;
- }
+ .dropdown-bold-header {
+ color: $gl-text-color-secondary;
+ font-size: 12px;
+ }
- .header-logo {
- display: inline-block;
- margin: 0 12px 0 2px;
- position: relative;
- top: 10px;
- transition-duration: .3s;
+ .navbar-collapse {
+ flex: 0 0 auto;
+ border-top: none;
+ padding: 0;
- svg,
- img {
- height: 28px;
- }
+ @media (max-width: $screen-xs-max) {
+ flex: 1 1 auto;
+ }
- &:hover {
- cursor: pointer;
+ .nav {
+ > li:not(.hidden-xs) a {
+ @media (max-width: $screen-xs-max) {
+ margin-left: 0;
+ min-width: 100%;
+ }
}
}
+ }
- .group-name-toggle {
- margin: 3px 5px;
- }
+ .container-fluid {
- .group-title {
- &.is-hidden {
- .hidable:not(:last-of-type) {
- display: none;
+ .navbar-nav {
+ @media (max-width: $screen-xs-max) {
+ display: -webkit-flex;
+ display: flex;
+ padding-right: 10px;
+ }
+
+ li {
+ .badge {
+ box-shadow: none;
+ font-weight: $gl-font-weight-bold;
}
}
}
- .title-container {
- display: flex;
- align-items: flex-start;
- flex: 1 1 auto;
- padding-top: 14px;
- overflow: hidden;
- }
+ .nav > li {
+ &.header-user {
+ @media (max-width: $screen-xs-max) {
+ padding-left: 10px;
+ }
+ }
- .title {
- position: relative;
- padding-right: 20px;
- margin: 0;
- font-size: 18px;
- line-height: 22px;
- display: inline-block;
- font-weight: $gl-font-weight-normal;
- color: $gl-text-color;
- vertical-align: top;
- white-space: nowrap;
+ > a {
+ will-change: color;
+ margin: 4px 2px;
+ padding: 6px 8px;
+ height: 32px;
- &.wrap {
- white-space: normal;
+ @media (max-width: $screen-xs-max) {
+ padding: 0;
+ }
+
+ &.header-user-dropdown-toggle {
+ margin-left: 2px;
+
+ .header-user-avatar {
+ margin-right: 0;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ outline: 0;
+ opacity: 1;
+ color: $white-light;
+
+ svg {
+ fill: currentColor;
+ }
+
+ &.header-user-dropdown-toggle {
+ .header-user-avatar {
+ border-color: $white-light;
+ }
+ }
+ }
}
- &.initializing {
- opacity: 0;
+ .header-new-dropdown-toggle {
+ margin-right: 0;
}
- a {
- color: currentColor;
+ .impersonated-user,
+ .impersonated-user:hover {
+ margin-right: 1px;
+ background-color: $white-light;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
- &:hover {
- text-decoration: underline;
- color: $gl-header-nav-hover-color;
+ .impersonation-btn,
+ .impersonation-btn:hover {
+ background-color: $white-light;
+ margin-left: 0;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+
+ i {
+ color: $orange-500;
+ font-size: 20px;
}
}
- .dropdown-toggle-caret {
- color: $gl-text-color;
- border: transparent;
- background: transparent;
- position: absolute;
- top: 2px;
- right: 3px;
- width: 12px;
- line-height: 19px;
- padding: 0;
- font-size: 10px;
- text-align: center;
- cursor: pointer;
+ &.active > a,
+ &.dropdown.open > a {
- &:hover {
- color: $gl-header-nav-hover-color;
+ svg {
+ fill: currentColor;
}
}
+ }
+ }
+}
- .project-item-select {
- right: auto;
- left: 0;
+.navbar-sub-nav,
+.navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus {
+ text-decoration: none;
+ outline: 0;
+ color: $white-light;
+
+ svg {
+ fill: currentColor;
}
}
- .navbar-collapse {
- flex: 0 0 auto;
- border-top: none;
- padding: 0;
-
- @media (max-width: $screen-xs-max) {
- flex: 1 1 auto;
+ > a {
+ display: -webkit-flex;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 8px;
+ margin: 4px 2px;
+ font-size: 12px;
+ color: currentColor;
+ border-radius: $border-radius-default;
+ height: 32px;
+ font-weight: $gl-font-weight-bold;
+
+ svg {
+ fill: currentColor;
}
}
+
+ &.line-separator {
+ margin: 8px;
+ }
+ }
+}
+
+.navbar-sub-nav {
+ display: -webkit-flex;
+ display: flex;
+ margin: 0 0 0 6px;
+
+ .projects-dropdown-menu {
+ padding: 0;
+ }
+
+ .dropdown-chevron {
+ position: relative;
+ top: -1px;
+ font-size: 10px;
}
.project-item-select-holder {
@@ -314,8 +344,123 @@ header {
}
}
-.with-performance-bar header.navbar-gitlab {
- top: $performance-bar-height;
+.caret-down {
+ height: 11px;
+ width: 11px;
+ margin-left: 4px;
+ fill: currentColor;
+}
+
+.header-user .dropdown-menu-nav,
+.header-new .dropdown-menu-nav {
+ margin-top: $dropdown-vertical-offset;
+}
+
+.breadcrumbs {
+ display: -webkit-flex;
+ display: flex;
+ min-height: 48px;
+ color: $gl-text-color;
+}
+
+.breadcrumbs-container {
+ display: -webkit-flex;
+ display: flex;
+ width: 100%;
+ position: relative;
+ padding-top: $gl-padding / 2;
+ padding-bottom: $gl-padding / 2;
+ align-items: center;
+ border-bottom: 1px solid $border-color;
+}
+
+.breadcrumbs-links {
+ -webkit-flex: 1;
+ flex: 1;
+ min-width: 0;
+ align-self: center;
+ color: $gl-text-color-secondary;
+
+ .avatar-tile {
+ margin-right: 4px;
+ border: 1px solid $border-color;
+ border-radius: 50%;
+ vertical-align: sub;
+ }
+
+ .text-expander {
+ margin-left: 0;
+ margin-right: 2px;
+
+ > i {
+ position: relative;
+ top: 1px;
+ }
+ }
+}
+
+.breadcrumbs-list {
+ display: -webkit-flex;
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 0;
+ line-height: 16px;
+
+ > li {
+ display: flex;
+ align-items: center;
+ position: relative;
+ padding: 2px 0;
+
+ &:not(:last-child) {
+ margin-right: 20px;
+ }
+
+ > a {
+ font-size: 12px;
+ color: currentColor;
+ }
+ }
+}
+
+.breadcrumb-item-text {
+ @include str-truncated(128px);
+ text-decoration: inherit;
+}
+
+.breadcrumbs-list-angle {
+ position: absolute;
+ right: -12px;
+ top: 50%;
+ color: $gl-text-color-tertiary;
+ transform: translateY(-50%);
+}
+
+.breadcrumbs-extra {
+ display: -webkit-flex;
+ display: flex;
+ flex: 0 0 auto;
+ margin-left: auto;
+}
+
+.breadcrumbs-sub-title {
+ margin: 0;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 16px;
+
+ a {
+ color: $gl-text-color;
+ }
+}
+
+.btn-sign-in {
+ margin-top: 3px;
+ font-weight: $gl-font-weight-bold;
+
+ &:hover {
+ background-color: $white-light;
+ }
}
.navbar-nav {
@@ -347,11 +492,10 @@ header {
}
@media (max-width: $screen-xs-max) {
- header .container-fluid {
+ .navbar-gitlab .container-fluid {
font-size: 18px;
.navbar-nav {
- display: table;
table-layout: fixed;
width: 100%;
margin: 0;
@@ -359,7 +503,8 @@ header {
}
.navbar-collapse {
- padding-left: 5px;
+ margin-left: -8px;
+ margin-right: -10px;
.nav > li:not(.hidden-xs) {
display: table-cell !important;
@@ -385,11 +530,11 @@ header {
.dropdown-menu-nav {
width: auto;
min-width: 140px;
- margin-top: -5px;
+ margin-top: 4px;
color: $gl-text-color;
left: auto;
- .current-user {
+ li.current-user {
padding: 5px 18px;
.user-name {
@@ -405,3 +550,23 @@ header {
border-radius: 50%;
border: 1px solid $avatar-border;
}
+
+.with-performance-bar .navbar-gitlab {
+ top: $performance-bar-height;
+}
+
+.navbar-empty {
+ height: $header-height;
+ background: $white-light;
+ border-bottom: 1px solid $white-normal;
+
+ .center-logo {
+ margin: 8px 0;
+ text-align: center;
+
+ .tanuki-logo,
+ img {
+ height: 36px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 59bfc5a8d77..6819fd88b7f 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -28,6 +28,7 @@
svg {
&.s8 { @include svg-size(8px); }
+ &.s12 { @include svg-size(12px); }
&.s16 { @include svg-size(16px); }
&.s18 { @include svg-size(18px); }
&.s24 { @include svg-size(24px); }
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index bd521028c44..69d19ea2962 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -25,10 +25,6 @@ body {
.content-wrapper {
padding-bottom: 100px;
-
- &:not(.page-with-layout-nav) {
- margin-top: $header-height;
- }
}
.container {
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 5b581780447..1cebd02df48 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -1,10 +1,17 @@
+.modal-header {
+ padding: #{3 * $grid-size} #{2 * $grid-size};
+
+ .page-title {
+ margin-top: 0;
+ }
+}
+
.modal-body {
position: relative;
- padding: 15px;
+ padding: #{3 * $grid-size} #{2 * $grid-size};
.form-actions {
- margin: -$gl-padding + 1;
- margin-top: 15px;
+ margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size};
}
.text-danger {
diff --git a/app/assets/stylesheets/framework/new-nav.scss b/app/assets/stylesheets/framework/new-nav.scss
index 7899be2c2d3..e69de29bb2d 100644
--- a/app/assets/stylesheets/framework/new-nav.scss
+++ b/app/assets/stylesheets/framework/new-nav.scss
@@ -1,404 +0,0 @@
-@import "framework/variables";
-@import 'framework/tw_bootstrap_variables';
-@import "bootstrap/variables";
-@import "framework/mixins";
-
-.content-wrapper.page-with-new-nav {
- margin-top: $new-navbar-height;
-}
-
-header.navbar-gitlab-new {
- color: $white-light;
- border-bottom: 0;
- min-height: $new-navbar-height;
-
- .logo-text {
- line-height: initial;
-
- svg {
- width: 55px;
- height: 14px;
- margin: 0;
- fill: $white-light;
- }
- }
-
- .header-content {
- display: -webkit-flex;
- display: flex;
- padding-left: 0;
- min-height: $new-navbar-height;
-
- .title-container {
- display: -webkit-flex;
- display: flex;
- -webkit-align-items: stretch;
- align-items: stretch;
- -webkit-flex: 1 1 auto;
- flex: 1 1 auto;
- padding-top: 0;
- overflow: visible;
- }
-
- .title {
- display: -webkit-flex;
- display: flex;
- padding-right: 0;
- color: currentColor;
-
- img {
- height: 28px;
- margin-right: 8px;
- }
-
- a {
- display: -webkit-flex;
- display: flex;
- align-items: center;
- padding: 2px 8px;
- margin: 5px 2px 5px -8px;
- border-radius: $border-radius-default;
-
- svg {
- @media (min-width: $screen-sm-min) {
- margin-right: 8px;
- }
- }
- }
- }
-
- .dropdown.open {
- > a {
- border-bottom-color: $white-light;
- }
- }
-
- .dropdown-menu {
- margin-top: 4px;
- min-width: 130px;
-
- @media (max-width: $screen-xs-max) {
- left: auto;
- right: 0;
- }
- }
-
- &.menu-expanded {
- @media (max-width: $screen-xs-max) {
- .title-container,
- .header-logo, {
- display: none;
- }
- }
- }
- }
-
- .dropdown-bold-header {
- color: $gl-text-color-secondary;
- font-size: 12px;
- }
-
- .navbar-collapse {
- padding-left: 0;
- box-shadow: 0;
-
- @media (max-width: $screen-xs-max) {
- margin-left: -8px;
- margin-right: -10px;
- }
-
- .nav {
- > li:not(.hidden-xs) a {
- @media (max-width: $screen-xs-max) {
- margin-left: 0;
- min-width: 100%;
- }
- }
- }
- }
-
- .container-fluid {
- .navbar-toggle {
- min-width: 45px;
- padding: 0 $gl-padding;
- margin-right: -7px;
- text-align: center;
- color: currentColor;
-
- svg {
- fill: currentColor;
- }
-
- &:hover,
- &:focus,
- &.active {
- color: currentColor;
- background-color: transparent;
-
- svg {
- fill: currentColor;
- }
- }
- }
-
- .navbar-nav {
- @media (max-width: $screen-xs-max) {
- display: flex;
- padding-right: 10px;
- }
-
- li {
- .badge {
- box-shadow: none;
- font-weight: $gl-font-weight-bold;
- }
- }
- }
-
- .nav > li {
- &.header-user {
- @media (max-width: $screen-xs-max) {
- padding-left: 10px;
- }
- }
-
- > a {
- will-change: color;
- margin: 4px 2px;
- padding: 6px 8px;
- height: 32px;
-
- @media (max-width: $screen-xs-max) {
- padding: 0;
- }
-
- &.header-user-dropdown-toggle {
- margin-left: 2px;
-
- .header-user-avatar {
- margin-right: 0;
- }
- }
-
- &:hover,
- &:focus {
- text-decoration: none;
- outline: 0;
- opacity: 1;
- color: $white-light;
-
- svg {
- fill: currentColor;
- }
-
- &.header-user-dropdown-toggle {
- .header-user-avatar {
- border-color: $white-light;
- }
- }
- }
- }
-
- .header-new-dropdown-toggle {
- margin-right: 0;
- }
-
- .impersonated-user,
- .impersonated-user:hover {
- margin-right: 1px;
- background-color: $white-light;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- }
-
- .impersonation-btn,
- .impersonation-btn:hover {
- background-color: $white-light;
- margin-left: 0;
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
-
- i {
- color: $orange-500;
- font-size: 20px;
- }
- }
-
- &.active > a,
- &.dropdown.open > a {
-
- svg {
- fill: currentColor;
- }
- }
- }
- }
-}
-
-.navbar-sub-nav {
- display: -webkit-flex;
- display: flex;
- margin: 0 0 0 6px;
-
- .dropdown-chevron {
- position: relative;
- top: -1px;
- font-size: 10px;
- }
-}
-
-.navbar-gitlab-new {
- .navbar-sub-nav,
- .navbar-nav {
- > li {
- > a:hover,
- > a:focus {
- text-decoration: none;
- outline: 0;
- color: $white-light;
-
- svg {
- fill: currentColor;
- }
- }
-
- > a {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 6px 8px;
- margin: 4px 2px;
- font-size: 12px;
- color: currentColor;
- border-radius: $border-radius-default;
- height: 32px;
- font-weight: $gl-font-weight-bold;
-
- svg {
- fill: currentColor;
- }
- }
-
- &.line-separator {
- margin: 8px;
- }
- }
- }
-}
-
-.caret-down {
- height: 11px;
- width: 11px;
- margin-left: 4px;
- fill: currentColor;
-}
-
-.header-user .dropdown-menu-nav,
-.header-new .dropdown-menu-nav {
- margin-top: $dropdown-vertical-offset;
-}
-
-.breadcrumbs {
- display: flex;
- min-height: 48px;
- color: $gl-text-color;
-}
-
-.breadcrumbs-container {
- display: -webkit-flex;
- display: flex;
- width: 100%;
- position: relative;
- padding-top: $gl-padding / 2;
- padding-bottom: $gl-padding / 2;
- align-items: center;
- border-bottom: 1px solid $border-color;
-}
-
-.breadcrumbs-links {
- -webkit-flex: 1;
- flex: 1;
- min-width: 0;
- align-self: center;
- color: $gl-text-color-secondary;
-
- .avatar-tile {
- margin-right: 4px;
- border: 1px solid $border-color;
- border-radius: 50%;
- vertical-align: sub;
- }
-
- .text-expander {
- margin-left: 0;
- margin-right: 2px;
-
- > i {
- position: relative;
- top: 1px;
- }
- }
-}
-
-.breadcrumbs-list {
- display: -webkit-flex;
- display: flex;
- flex-wrap: wrap;
- margin-bottom: 0;
- line-height: 16px;
-
- > li {
- display: flex;
- align-items: center;
- position: relative;
- padding: 2px 0;
-
- &:not(:last-child) {
- margin-right: 20px;
- }
-
- > a {
- font-size: 12px;
- color: currentColor;
- }
- }
-}
-
-.breadcrumb-item-text {
- @include str-truncated(128px);
- text-decoration: inherit;
-}
-
-.breadcrumbs-list-angle {
- position: absolute;
- right: -12px;
- top: 50%;
- color: $gl-text-color-tertiary;
- transform: translateY(-50%);
-}
-
-.breadcrumbs-extra {
- display: flex;
- flex: 0 0 auto;
- margin-left: auto;
-}
-
-.breadcrumbs-sub-title {
- margin: 0;
- font-size: 12px;
- font-weight: 600;
- line-height: 16px;
-
- a {
- color: $gl-text-color;
- }
-}
-
-.btn-sign-in {
- margin-top: 3px;
- font-weight: $gl-font-weight-bold;
-
- &:hover {
- background-color: $white-light;
- }
-}
diff --git a/app/assets/stylesheets/framework/new-sidebar.scss b/app/assets/stylesheets/framework/new-sidebar.scss
index 8332cec2962..caf4c7a40b1 100644
--- a/app/assets/stylesheets/framework/new-sidebar.scss
+++ b/app/assets/stylesheets/framework/new-sidebar.scss
@@ -24,7 +24,7 @@ $new-sidebar-collapsed-width: 50px;
// Override position: absolute
.right-sidebar {
position: fixed;
- height: calc(100% - #{$new-navbar-height});
+ height: calc(100% - #{$header-height});
}
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
@@ -87,7 +87,7 @@ $new-sidebar-collapsed-width: 50px;
z-index: 400;
width: $new-sidebar-width;
transition: left $sidebar-transition-duration;
- top: $new-navbar-height;
+ top: $header-height;
bottom: 0;
left: 0;
background-color: $gray-normal;
@@ -197,7 +197,7 @@ $new-sidebar-collapsed-width: 50px;
}
.with-performance-bar .nav-sidebar {
- top: $new-navbar-height + $performance-bar-height;
+ top: $header-height + $performance-bar-height;
}
.sidebar-sub-level-items {
@@ -495,7 +495,7 @@ $new-sidebar-collapsed-width: 50px;
// Make issue boards full-height now that sub-nav is gone
.boards-list {
- height: calc(100vh - #{$new-navbar-height});
+ height: calc(100vh - #{$header-height});
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
@@ -506,5 +506,5 @@ $new-sidebar-collapsed-width: 50px;
}
.with-performance-bar .boards-list {
- height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height});
+ height: calc(100vh - #{$header-height} - #{$performance-bar-height});
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
index f8777d1fd9d..5c96b3b78e7 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
@@ -1,12 +1,11 @@
-
-
+// For tabbed navigation links, scrolling tabs, etc. For all top/main navigation,
+// please check nav.scss
.nav-links {
display: flex;
padding: 0;
margin: 0;
list-style: none;
height: auto;
- border-bottom: 1px solid $border-color;
li {
display: flex;
@@ -24,7 +23,6 @@
&:active,
&:focus {
text-decoration: none;
- border-bottom: 2px solid $gray-darkest;
color: $black;
.badge {
@@ -34,7 +32,6 @@
}
&.active a {
- border-bottom: 2px solid $link-underline-blue;
color: $black;
font-weight: $gl-font-weight-bold;
@@ -43,35 +40,6 @@
}
}
}
-
- &.sub-nav {
- text-align: center;
- background-color: $gray-normal;
-
- .container-fluid {
- background-color: $gray-normal;
- margin-bottom: 0;
- display: flex;
- }
-
- li {
- &.active a {
- border-bottom: none;
- color: $link-underline-blue;
- }
-
- a {
- margin: 0;
- padding: 11px 10px 9px;
-
- &:hover,
- &:active,
- &:focus {
- border-color: transparent;
- }
- }
- }
- }
}
.top-area {
@@ -91,17 +59,6 @@
}
}
- .nav-search {
- display: inline-block;
- width: 100%;
- padding: 11px 0;
-
- /* Small devices (phones, tablets, 768px and lower) */
- @media (min-width: $screen-sm-min) {
- width: 50%;
- }
- }
-
.nav-links {
margin-bottom: 0;
border-bottom: none;
@@ -150,12 +107,6 @@
}
}
- &.nav-controls-new-nav {
- > .dropdown {
- margin-right: 0;
- }
- }
-
> .btn-grouped {
float: none;
}
@@ -248,114 +199,43 @@
pre {
width: 100%;
}
-}
-
-.project-item-select-holder.btn-group {
- display: flex;
- max-width: 350px;
- overflow: hidden;
- float: right;
-
- .new-project-item-link {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .new-project-item-select-button {
- width: 32px;
- }
-}
-
-.empty-state .project-item-select-holder.btn-group {
- float: none;
- display: inline-block;
-
- .btn {
- // overrides styles applied to plain `.empty-state .btn`
- margin: 10px 0;
- max-width: 300px;
- width: auto;
-
- @media(max-width: $screen-xs-max) {
- max-width: 250px;
- }
-
- }
-}
-.new-project-item-select-button .fa-caret-down {
- margin-left: 2px;
-}
-
-.layout-nav {
- width: 100%;
- background: $gray-light;
- border-bottom: 1px solid $border-color;
- transition: padding $sidebar-transition-duration;
- text-align: center;
- margin-top: $new-navbar-height;
+ @media (max-width: $screen-xs-max) {
+ flex-flow: row wrap;
- .container-fluid {
- position: relative;
+ .nav-controls {
+ $controls-margin: $btn-xs-side-margin - 2px;
+ flex: 0 0 100%;
- .nav-control {
- @media (max-width: $screen-sm-max) {
- margin-right: 2px;
+ &.controls-flex {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: center;
+ padding: 0 0 $gl-padding-top;
}
- }
- }
-
- .controls {
- float: right;
- padding: 7px 0 0;
-
- i {
- color: $layout-link-gray;
- }
-
- .fa-rss,
- .fa-cog {
- font-size: 16px;
- }
- .fa-caret-down {
- margin-left: 5px;
- color: $gl-text-color-secondary;
- }
-
- .dropdown {
- position: absolute;
- top: 7px;
- right: 15px;
- z-index: 300;
+ .controls-item,
+ .controls-item-full,
+ .controls-item:last-child {
+ flex: 1 1 35%;
+ display: block;
+ width: 100%;
+ margin: $controls-margin;
- li.active {
- font-weight: $gl-font-weight-bold;
+ .btn,
+ .dropdown {
+ margin: 0;
+ }
}
- }
- }
- .nav-links {
- border-bottom: none;
- height: 51px;
-
- @media (min-width: $screen-sm-min) {
- justify-content: center;
- }
-
- li {
- a {
- padding-top: 10px;
+ .controls-item-full {
+ flex: 1 1 100%;
}
}
}
}
-.with-performance-bar .layout-nav {
- margin-top: $header-height + $performance-bar-height;
-}
-
.scrolling-tabs-container {
position: relative;
@@ -385,25 +265,41 @@
left: -7px;
}
}
+}
- &.sub-nav-scroll {
+.inner-page-scroll-tabs {
+ position: relative;
- .fade-right {
- @include fade(left, $gray-normal);
- right: 0;
+ .fade-right {
+ @include fade(left, $white-light);
+ right: 0;
+ text-align: right;
- .fa {
- right: -23px;
- }
+ .fa {
+ right: 5px;
}
+ }
- .fade-left {
- @include fade(right, $gray-normal);
- left: 0;
+ .fade-left {
+ @include fade(right, $white-light);
+ left: 0;
+ text-align: left;
- .fa {
- left: 10px;
- }
+ .fa {
+ left: 5px;
+ }
+ }
+
+ .fade-right,
+ .fade-left {
+ top: 16px;
+ bottom: auto;
+ }
+
+ &.is-smaller {
+ .fade-right,
+ .fade-left {
+ top: 11px;
}
}
}
@@ -432,41 +328,7 @@
}
}
}
-}
-
-.page-with-layout-nav {
- .right-sidebar {
- top: ($header-height + 1) * 2;
- }
-
- &.page-with-sub-nav {
- .right-sidebar {
- top: ($header-height + 1) * 3;
-
- &.affix {
- top: $header-height;
- }
- }
- }
-}
-
-.with-performance-bar .page-with-layout-nav {
- .right-sidebar {
- top: ($header-height + 1) * 2 + $performance-bar-height;
- }
-
- &.page-with-sub-nav {
- .right-sidebar {
- top: ($header-height + 1) * 3 + $performance-bar-height;
-
- &.affix {
- top: $header-height + $performance-bar-height;
- }
- }
- }
-}
-.nav-block {
&.activities {
border-bottom: 1px solid $border-color;
@@ -476,76 +338,39 @@
}
}
-@media (max-width: $screen-xs-max) {
- .top-area {
- flex-flow: row wrap;
-
- .nav-controls {
- $controls-margin: $btn-xs-side-margin - 2px;
- flex: 0 0 100%;
-
- &.controls-flex {
- display: flex;
- flex-flow: row wrap;
- align-items: center;
- justify-content: center;
- padding: 0 0 $gl-padding-top;
- }
-
- .controls-item,
- .controls-item-full,
- .controls-item:last-child {
- flex: 1 1 35%;
- display: block;
- width: 100%;
- margin: $controls-margin;
+.project-item-select-holder.btn-group {
+ display: flex;
+ max-width: 350px;
+ overflow: hidden;
+ float: right;
- .btn,
- .dropdown {
- margin: 0;
- }
- }
+ .new-project-item-link {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
- .controls-item-full {
- flex: 1 1 100%;
- }
- }
+ .new-project-item-select-button {
+ width: 32px;
}
}
-.inner-page-scroll-tabs {
- position: relative;
-
- .fade-right {
- @include fade(left, $white-light);
- right: 0;
- text-align: right;
-
- .fa {
- right: 5px;
- }
- }
+.empty-state .project-item-select-holder.btn-group {
+ float: none;
+ display: inline-block;
- .fade-left {
- @include fade(right, $white-light);
- left: 0;
- text-align: left;
+ .btn {
+ // overrides styles applied to plain `.empty-state .btn`
+ margin: 10px 0;
+ max-width: 300px;
+ width: auto;
- .fa {
- left: 5px;
+ @media(max-width: $screen-xs-max) {
+ max-width: 250px;
}
}
+}
- .fade-right,
- .fade-left {
- top: 16px;
- bottom: auto;
- }
-
- &.is-smaller {
- .fade-right,
- .fade-left {
- top: 11px;
- }
- }
+.new-project-item-select-button .fa-caret-down {
+ margin-left: 2px;
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 48dc25d343b..ef58382ba41 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -78,16 +78,16 @@
.right-sidebar {
border-left: 1px solid $border-color;
- height: calc(100% - #{$new-navbar-height});
+ height: calc(100% - #{$header-height});
&.affix {
position: fixed;
- top: $new-navbar-height;
+ top: $header-height;
}
}
.with-performance-bar .right-sidebar.affix {
- top: $new-navbar-height + $performance-bar-height;
+ top: $header-height + $performance-bar-height;
}
@mixin maintain-sidebar-dimensions {
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 3d68a50f91f..f718ec4bcad 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -17,15 +17,19 @@
.diff-file {
border: 1px solid $border-color;
- border-bottom: none;
margin: 0;
}
+
+ &.text-file .diff-file {
+ border-bottom: none;
+ }
}
.timeline-entry {
border-color: $white-normal;
color: $gl-text-color;
border-bottom: 1px solid $border-white-light;
+ background: $white-light;
.timeline-entry-inner {
position: relative;
diff --git a/app/assets/stylesheets/framework/tooltips.scss b/app/assets/stylesheets/framework/tooltips.scss
new file mode 100644
index 00000000000..93baf73cb78
--- /dev/null
+++ b/app/assets/stylesheets/framework/tooltips.scss
@@ -0,0 +1,7 @@
+.tooltip-inner {
+ font-size: $tooltip-font-size;
+ border-radius: $border-radius-default;
+ line-height: 16px;
+ font-weight: $gl-font-weight-normal;
+ padding: $gl-btn-padding;
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 60260355765..bbbd16322eb 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,6 +1,7 @@
/*
* Layout
*/
+$grid-size: 8px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
@@ -203,6 +204,11 @@ $code_font_size: 12px;
$code_line_height: 1.6;
/*
+ * Tooltips
+ */
+$tooltip-font-size: 12px;
+
+/*
* Padding
*/
$gl-padding: 16px;
@@ -219,8 +225,7 @@ $gl-sidebar-padding: 22px;
$row-hover: $blue-50;
$row-hover-border: $blue-200;
$progress-color: #c0392b;
-$header-height: 50px;
-$new-navbar-height: 40px;
+$header-height: 40px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
@@ -317,6 +322,7 @@ $diff-image-info-color: grey;
$diff-swipe-border: #999;
$diff-view-modes-color: grey;
$diff-view-modes-border: #c1c1c1;
+$diff-jagged-border-gradient-color: darken($white-normal, 8%);
/*
* Fonts
@@ -700,3 +706,15 @@ Project Templates Icons
$rails: #c00;
$node: #353535;
$java: #70ad51;
+
+/*
+Issuable warning
+*/
+$issuable-warning-size: 24px;
+$issuable-warning-icon-margin: 4px;
+
+/*
+Image Commenting cursor
+*/
+$image-comment-cursor-left-offset: 12;
+$image-comment-cursor-top-offset: 30;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 3305a482a0d..ca61f7a30c3 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -414,7 +414,6 @@
margin: 5px;
}
-.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar,
.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar {
.issuable-sidebar-header {
position: relative;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 359dd388d05..50ec5110bf1 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -64,10 +64,10 @@
color: $gl-text-color;
position: sticky;
position: -webkit-sticky;
- top: $new-navbar-height;
+ top: $header-height;
&.affix {
- top: $new-navbar-height;
+ top: $header-height;
}
// with sidebar
@@ -174,10 +174,10 @@
.with-performance-bar .build-page {
.top-bar {
- top: $new-navbar-height + $performance-bar-height;
+ top: $header-height + $performance-bar-height;
&.affix {
- top: $new-navbar-height + $performance-bar-height;
+ top: $header-height + $performance-bar-height;
}
}
}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
new file mode 100644
index 00000000000..5538e46a6c4
--- /dev/null
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -0,0 +1,9 @@
+.edit-cluster-form {
+ .clipboard-addon {
+ background-color: $white-light;
+ }
+
+ .alert-block {
+ margin-bottom: 20px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 994707422bb..ee3ca246374 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -54,12 +54,15 @@
.mr-widget-pipeline-graph {
display: inline-block;
vertical-align: middle;
- margin-right: 4px;
.stage-cell .stage-container {
margin: 3px 3px 3px 0;
}
+ .stage-container:last-child {
+ margin-right: 0;
+ }
+
.dropdown-menu {
margin-top: 11px;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index fb23343b966..ffb5fc94475 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -297,6 +297,7 @@
.drag-track {
display: block;
position: absolute;
+ top: 0;
left: 12px;
height: 10px;
width: 276px;
@@ -547,16 +548,23 @@
}
.diff-notes-collapse {
- width: 19px;
- height: 19px;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
padding: 0;
transition: transform .1s ease-out;
z-index: 100;
+ .collapse-icon {
+ height: 50%;
+ width: 100%;
+ }
+
svg {
- vertical-align: text-top;
+ vertical-align: middle;
}
+ .collapse-icon,
path {
fill: $white-light;
}
@@ -644,3 +652,157 @@
text-overflow: ellipsis;
white-space: nowrap;
}
+
+.note-container {
+ background-color: $gray-light;
+ border-top: 1px solid $white-normal;
+
+ // double jagged line divider
+ .discussion-notes + .discussion-notes::before,
+ .discussion-notes + .discussion-form::before {
+ content: '';
+ position: relative;
+ display: block;
+ width: 100%;
+ height: 10px;
+ background-color: $white-light;
+ background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
+ linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
+ linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
+ linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%);
+ background-position: 5px 5px,0 5px,0 5px,5px 5px;
+ background-size: 10px 10px;
+ background-repeat: repeat;
+ }
+
+ .notes {
+ position: relative;
+ }
+
+ .diff-notes-collapse {
+ position: absolute;
+ left: -12px;
+ }
+}
+
+.diff-file .note-container > .new-note,
+.note-container .discussion-notes {
+ margin-left: 100px;
+ border-left: 1px solid $white-normal;
+}
+
+.notes.active {
+ .diff-file .note-container > .new-note,
+ .note-container .discussion-notes {
+ // Override our margin and border (set for diff tab)
+ // when user is on the discussion tab for MR
+ margin-left: inherit;
+ border-left: inherit;
+ }
+}
+
+.files:not([data-can-create-note]) .frame {
+ cursor: auto;
+}
+
+.frame.click-to-comment {
+ position: relative;
+ cursor: url(icon_image_comment.svg)
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
+
+ // Retina cursor
+ cursor: -webkit-image-set(url(icon_image_comment.svg) 1x, url(icon_image_comment@2x.svg) 2x)
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
+
+ .comment-indicator {
+ position: absolute;
+ padding: 0;
+ width: (2px * $image-comment-cursor-left-offset);
+ height: (1px * $image-comment-cursor-top-offset);
+ // center the indicator to match the top left click region
+ margin-top: (-1px * $image-comment-cursor-top-offset) + 2;
+ margin-left: (-1px * $image-comment-cursor-left-offset) + 1;
+
+ svg {
+ width: 100%;
+ height: 100%;
+ }
+
+ &:focus {
+ outline: none;
+ }
+ }
+}
+
+.frame .badge,
+.image-diff-avatar-link .badge,
+.notes > .badge {
+ position: absolute;
+ background-color: $blue-400;
+ color: $white-light;
+ border: $white-light 1px solid;
+ min-height: $gl-padding;
+ padding: 5px 8px;
+ border-radius: 12px;
+
+ &:focus {
+ outline: none;
+ }
+}
+
+.frame .badge,
+.frame .image-comment-badge {
+ // Center align badges on the frame
+ transform: translate3d(-50%, -50%, 0);
+}
+
+.image-comment-badge {
+ @include btn-comment-icon;
+ position: absolute;
+
+ &.inverted {
+ border-color: $white-light;
+ }
+}
+
+.image-diff-avatar-link {
+ position: relative;
+
+ .badge,
+ .image-comment-badge {
+ top: 25px;
+ right: 8px;
+ }
+}
+
+.notes > .badge {
+ display: none;
+ left: -13px;
+}
+
+.discussion-notes {
+ min-height: 35px;
+
+ &:first-child {
+ // First child does not have the jagged borders
+ min-height: 25px;
+ }
+
+ &.collapsed {
+ background-color: $white-light;
+
+ .diff-notes-collapse,
+ .note,
+ .discussion-reply-holder, {
+ display: none;
+ }
+
+ .notes > .badge {
+ display: block;
+ }
+ }
+}
+
+.discussion-body .image .frame {
+ position: relative;
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 9362d80d4e6..3b5e411e2c5 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -207,10 +207,13 @@
}
.prometheus-state {
- margin-top: 10px;
+ max-width: 430px;
+ margin: 10px auto;
+ text-align: center;
- .state-button-section {
- margin-top: 10px;
+ .state-svg {
+ max-width: 80vw;
+ margin: 0 auto;
}
}
@@ -288,8 +291,14 @@
fill: $black;
}
- .tick > text {
- font-size: 12px;
+ .tick {
+ > line {
+ stroke: $gray-darker;
+ }
+
+ > text {
+ font-size: 12px;
+ }
}
.text-metric-title {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 7eb28354e6d..dae3ec7ac42 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -5,27 +5,29 @@
margin-right: auto;
}
-.is-confidential {
+.issuable-warning-icon {
color: $orange-600;
background-color: $orange-100;
border-radius: $border-radius-default;
padding: 5px;
- margin: 0 3px 0 -4px;
+ margin: 0 $btn-side-margin 0 0;
+ width: $issuable-warning-size;
+ height: $issuable-warning-size;
+ text-align: center;
+
+ &:first-of-type {
+ margin-right: $issuable-warning-icon-margin;
+ }
}
-.is-not-confidential {
+.sidebar-item-icon {
border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
-}
-
-.confidentiality {
- .is-not-confidential {
- margin: auto;
- }
- .is-confidential {
- margin: auto;
+ &.is-active {
+ color: $orange-600;
+ background-color: $orange-50;
}
}
@@ -220,7 +222,7 @@
.right-sidebar {
position: absolute;
- top: $new-navbar-height;
+ top: $header-height;
bottom: 0;
right: 0;
transition: width $right-sidebar-transition-duration;
@@ -485,10 +487,10 @@
}
.with-performance-bar .right-sidebar {
- top: $new-navbar-height + $performance-bar-height;
+ top: $header-height + $performance-bar-height;
.issuable-sidebar {
- height: calc(100% - #{$new-navbar-height} - #{$performance-bar-height});
+ height: calc(100% - #{$header-height} - #{$performance-bar-height});
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 09a14578dd3..d9fb3b44d29 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -649,7 +649,7 @@
}
.merge-request-tabs-holder {
- top: $new-navbar-height;
+ top: $header-height;
z-index: 200;
background-color: $white-light;
border-bottom: 1px solid $border-color;
@@ -679,7 +679,7 @@
}
.with-performance-bar .merge-request-tabs-holder {
- top: $new-navbar-height + $performance-bar-height;
+ top: $header-height + $performance-bar-height;
}
.merge-request-tabs {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 74d9acb5490..04b132415eb 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -101,7 +101,7 @@
}
}
-.confidential-issue-warning {
+.issuable-note-warning {
color: $orange-600;
background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0;
@@ -110,37 +110,64 @@
padding: 3px 12px;
margin: auto;
align-items: center;
+
+ .icon {
+ margin-right: $issuable-warning-icon-margin;
+ }
+}
+
+.disabled-comment .issuable-note-warning {
+ border: none;
+ border-radius: $label-border-radius;
+ padding-top: $gl-vert-padding;
+ padding-bottom: $gl-vert-padding;
+
+ .icon svg {
+ position: relative;
+ top: 2px;
+ margin-right: $btn-xs-side-margin;
+ width: $gl-font-size;
+ height: $gl-font-size;
+ fill: $orange-600;
+ }
}
-.confidential-value {
+.sidebar-item-value {
.fa {
background-color: inherit;
}
}
-.confidential-warning-message {
+.sidebar-item-warning-message {
line-height: 1.5;
padding: 16px;
- .confidential-warning-message-actions {
+ .text {
+ color: $text-color;
+ }
+
+ .sidebar-item-warning-message-actions {
display: flex;
- button {
+ .btn {
flex-grow: 1;
}
}
}
-.confidential-issue-warning + .md-area {
+.issuable-note-warning + .md-area {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.discussion-form {
- padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light;
}
+.discussion-form-container {
+ padding: $gl-padding-top $gl-padding $gl-padding;
+}
+
.discussion-notes .disabled-comment {
padding: 6px 0;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 46d31e41ada..96b7db3b85d 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -650,29 +650,12 @@ ul.notes {
}
.add-diff-note {
+ @include btn-comment-icon;
opacity: 0;
margin-top: -2px;
- border-radius: 50%;
- background: $white-light;
- padding: 1px 5px;
- font-size: 12px;
- color: $blue-500;
margin-left: -55px;
position: absolute;
z-index: 10;
- width: 23px;
- height: 23px;
- border: 1px solid $blue-500;
-
- &:hover {
- background: $blue-500;
- border-color: $blue-600;
- color: $white-light;
- }
-
- &:active {
- outline: 0;
- }
}
.discussion-body,
@@ -703,6 +686,12 @@ ul.notes {
color: $note-disabled-comment-color;
padding: 90px 0;
+ &.discussion-locked {
+ border: none;
+ background-color: $white-light;
+ }
+
+
a {
color: $gl-link-color;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 086dd528579..8fc7a5eec9b 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -209,9 +209,11 @@
}
.stage-cell {
- @media (min-width: $screen-md-min) {
- min-width: 148px;
- margin-right: -4px;
+ &.table-section {
+ @media (min-width: $screen-md-min) {
+ min-width: 148px;
+ margin-right: -4px;
+ }
}
.mini-pipeline-graph-dropdown-toggle svg {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 67abe6e88ed..eab39f698c3 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -108,6 +108,15 @@
}
}
+.subkeys-list {
+ @include basic-list;
+
+ li {
+ padding: 3px 0;
+ border: none;
+ }
+}
+
.key-list-item {
.key-list-item-info {
@media (min-width: $screen-sm-min) {
@@ -392,11 +401,11 @@ table.u2f-registrations {
}
}
-.gpg-email-badge {
+.email-badge {
display: inline;
margin-right: $gl-padding / 2;
- .gpg-email-badge-email {
+ .email-badge-email {
display: inline;
margin-right: $gl-padding / 4;
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 89ebe3f9917..db0a04a5eb3 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -47,6 +47,7 @@ input[type="checkbox"]:hover {
}
.location-badge {
+ height: 32px;
font-size: 12px;
margin: -4px 4px -4px -4px;
line-height: 25px;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 224eee90a3f..e2f6e511c86 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -169,6 +169,14 @@
}
}
+ .tree-item-file-external-link {
+ margin-right: 4px;
+
+ span {
+ text-decoration: inherit;
+ }
+ }
+
.tree_commit {
max-width: 320px;
diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb
index a4648b33cfa..c27f2ee3c09 100644
--- a/app/controllers/admin/application_controller.rb
+++ b/app/controllers/admin/application_controller.rb
@@ -3,9 +3,23 @@
# Automatically sets the layout and ensures an administrator is logged in
class Admin::ApplicationController < ApplicationController
before_action :authenticate_admin!
+ before_action :display_read_only_information
layout 'admin'
def authenticate_admin!
render_404 unless current_user.admin?
end
+
+ def display_read_only_information
+ return unless Gitlab::Database.read_only?
+
+ flash.now[:notice] = read_only_message
+ end
+
+ private
+
+ # Overridden in EE
+ def read_only_message
+ _('You are on a read-only GitLab instance.')
+ end
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 676a7203c7d..156a8e2c515 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController
def remove_email
email = user.emails.find(params[:email_id])
- success = Emails::DestroyService.new(current_user, user: user, email: email.email).execute
+ success = Emails::DestroyService.new(current_user, user: user).execute(email)
respond_to do |format|
if success
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 771c6f3034a..967fe39256a 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -85,12 +85,21 @@ class ApplicationController < ActionController::Base
super
payload[:remote_ip] = request.remote_ip
- if current_user.present?
- payload[:user_id] = current_user.id
- payload[:username] = current_user.username
+ logged_user = auth_user
+
+ if logged_user.present?
+ payload[:user_id] = logged_user.try(:id)
+ payload[:username] = logged_user.try(:username)
end
end
+ # Controllers such as GitHttpController may use alternative methods
+ # (e.g. tokens) to authenticate the user, whereas Devise sets current_user
+ def auth_user
+ return current_user if current_user.present?
+ return try(:authenticated_user)
+ end
+
# This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 0d74078645a..737656b3dcc 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -10,7 +10,7 @@ module Boards
def index
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20)
- make_sure_position_is_set(issues)
+ make_sure_position_is_set(issues) if Gitlab::Database.read_write?
issues = issues.preload(:project,
:milestone,
:assignees,
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 915f32b4c33..1126f706393 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -96,7 +96,8 @@ module NotesActions
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
- note: note.note
+ note: note.note,
+ on_image: note.try(:on_image?)
)
discussion = note.to_discussion(noteable)
@@ -122,7 +123,9 @@ module NotesActions
def diff_discussion_html(discussion)
return unless discussion.diff_discussion?
- if params[:view] == 'parallel'
+ on_image = discussion.on_image?
+
+ if params[:view] == 'parallel' && !on_image
template = "discussions/_parallel_diff_discussion"
locals =
if params[:line_type] == 'old'
@@ -132,7 +135,9 @@ module NotesActions
end
else
template = "discussions/_diff_discussion"
- locals = { discussions: [discussion] }
+ @fresh_discussion = true
+
+ locals = { discussions: [discussion], on_image: on_image }
end
render_to_string(
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 0c2646d7bf0..80ab681ed87 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -10,13 +10,14 @@ class ConfirmationsController < Devise::ConfirmationsController
users_almost_there_path
end
- def after_confirmation_path_for(resource_name, resource)
- if signed_in?(resource_name)
+ def after_confirmation_path_for(_resource_name, resource)
+ # incoming resource can either be a :user or an :email
+ if signed_in?(:user)
after_sign_in(resource)
else
Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}")
flash[:notice] += " Please sign in."
- new_session_path(resource_name)
+ new_session_path(:user)
end
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index a8b2b93b458..02c5857eea7 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -7,9 +7,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def index
@sort = params[:sort]
@todos = @todos.page(params[:page])
- if @todos.out_of_range? && @todos.total_pages != 0
- redirect_to url_for(params.merge(page: @todos.total_pages, only_path: true))
- end
+
+ return if redirect_out_of_range(@todos)
end
def destroy
@@ -60,7 +59,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def find_todos
- @todos ||= TodosFinder.new(current_user, params).execute
+ @todos ||= TodosFinder.new(current_user, todo_params).execute
end
def todos_counts
@@ -69,4 +68,27 @@ class Dashboard::TodosController < Dashboard::ApplicationController
done_count: number_with_delimiter(current_user.todos_done_count)
}
end
+
+ def todo_params
+ params.permit(:action_id, :author_id, :project_id, :type, :sort, :state)
+ end
+
+ def redirect_out_of_range(todos)
+ total_pages =
+ if todo_params.except(:sort, :page).empty?
+ (current_user.todos_pending_count / todos.limit_value).ceil
+ else
+ todos.total_pages
+ end
+
+ return false if total_pages.zero?
+
+ out_of_range = todos.current_page > total_pages
+
+ if out_of_range
+ redirect_to url_for(params.merge(page: total_pages, only_path: true))
+ end
+
+ out_of_range
+ end
end
diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb
new file mode 100644
index 00000000000..5551057ff55
--- /dev/null
+++ b/app/controllers/google_api/authorizations_controller.rb
@@ -0,0 +1,29 @@
+module GoogleApi
+ class AuthorizationsController < ApplicationController
+ def callback
+ token, expires_at = GoogleApi::CloudPlatform::Client
+ .new(nil, callback_google_api_auth_url)
+ .get_token(params[:code])
+
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token] = token
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] =
+ expires_at.to_s
+
+ state_redirect_uri = redirect_uri_from_session_key(params[:state])
+
+ if state_redirect_uri
+ redirect_to state_redirect_uri
+ else
+ redirect_to root_path
+ end
+ end
+
+ private
+
+ def redirect_uri_from_session_key(state)
+ key = GoogleApi::CloudPlatform::Client
+ .session_key_for_redirect_uri(params[:state])
+ session[key] if key
+ end
+ end
+end
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index 97db84b92d4..bbd7ba49d77 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -1,15 +1,14 @@
class Profiles::EmailsController < Profiles::ApplicationController
+ before_action :find_email, only: [:destroy, :resend_confirmation_instructions]
+
def index
- @primary = current_user.email
+ @primary_email = current_user.email
@emails = current_user.emails.order_id_desc
end
def create
@email = Emails::CreateService.new(current_user, email_params.merge(user: current_user)).execute
-
- if @email.errors.blank?
- NotificationService.new.new_email(@email)
- else
+ unless @email.errors.blank?
flash[:alert] = @email.errors.full_messages.first
end
@@ -17,9 +16,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
end
def destroy
- @email = current_user.emails.find(params[:id])
-
- Emails::DestroyService.new(current_user, user: current_user, email: @email.email).execute
+ Emails::DestroyService.new(current_user, user: current_user).execute(@email)
respond_to do |format|
format.html { redirect_to profile_emails_url, status: 302 }
@@ -27,9 +24,23 @@ class Profiles::EmailsController < Profiles::ApplicationController
end
end
+ def resend_confirmation_instructions
+ if Emails::ConfirmService.new(current_user, user: current_user).execute(@email)
+ flash[:notice] = "Confirmation email sent to #{@email.email}"
+ else
+ flash[:alert] = "There was a problem sending the confirmation email"
+ end
+
+ redirect_to profile_emails_url
+ end
+
private
def email_params
params.require(:email).permit(:email)
end
+
+ def find_email
+ @email = current_user.emails.find(params[:id])
+ end
end
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
index 689c76059f6..38e3eacd229 100644
--- a/app/controllers/profiles/gpg_keys_controller.rb
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -2,7 +2,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
before_action :set_gpg_key, only: [:destroy, :revoke]
def index
- @gpg_keys = current_user.gpg_keys
+ @gpg_keys = current_user.gpg_keys.with_subkeys
@gpg_key = GpgKey.new
end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index eb010923466..0837451cc49 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -29,13 +29,17 @@ class Projects::ArtifactsController < Projects::ApplicationController
blob = @entry.blob
conditionally_expand_blob(blob)
- respond_to do |format|
- format.html do
- render 'file'
- end
-
- format.json do
- render_blob_json(blob)
+ if blob.external_link?(build)
+ redirect_to blob.external_url(@project, build)
+ else
+ respond_to do |format|
+ format.html do
+ render 'file'
+ end
+
+ format.json do
+ render_blob_json(blob)
+ end
end
end
end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
new file mode 100644
index 00000000000..03019b0becc
--- /dev/null
+++ b/app/controllers/projects/clusters_controller.rb
@@ -0,0 +1,136 @@
+class Projects::ClustersController < Projects::ApplicationController
+ before_action :cluster, except: [:login, :index, :new, :create]
+ before_action :authorize_read_cluster!
+ before_action :authorize_create_cluster!, only: [:new, :create]
+ before_action :authorize_google_api, only: [:new, :create]
+ before_action :authorize_update_cluster!, only: [:update]
+ before_action :authorize_admin_cluster!, only: [:destroy]
+
+ def index
+ if project.cluster
+ redirect_to project_cluster_path(project, project.cluster)
+ else
+ redirect_to new_project_cluster_path(project)
+ end
+ end
+
+ def login
+ begin
+ state = generate_session_key_redirect(namespace_project_clusters_url.to_s)
+
+ @authorize_url = GoogleApi::CloudPlatform::Client.new(
+ nil, callback_google_api_auth_url,
+ state: state).authorize_url
+ rescue GoogleApi::Auth::ConfigMissingError
+ # no-op
+ end
+ end
+
+ def new
+ @cluster = project.build_cluster
+ end
+
+ def create
+ @cluster = Ci::CreateClusterService
+ .new(project, current_user, create_params)
+ .execute(token_in_session)
+
+ if @cluster.persisted?
+ redirect_to project_cluster_path(project, @cluster)
+ else
+ render :new
+ end
+ end
+
+ def status
+ respond_to do |format|
+ format.json do
+ Gitlab::PollingInterval.set_header(response, interval: 10_000)
+
+ render json: ClusterSerializer
+ .new(project: @project, current_user: @current_user)
+ .represent_status(@cluster)
+ end
+ end
+ end
+
+ def show
+ end
+
+ def update
+ Ci::UpdateClusterService
+ .new(project, current_user, update_params)
+ .execute(cluster)
+
+ if cluster.valid?
+ flash[:notice] = "Cluster was successfully updated."
+ redirect_to project_cluster_path(project, project.cluster)
+ else
+ render :show
+ end
+ end
+
+ def destroy
+ if cluster.destroy
+ flash[:notice] = "Cluster integration was successfully removed."
+ redirect_to project_clusters_path(project), status: 302
+ else
+ flash[:notice] = "Cluster integration was not removed."
+ render :show
+ end
+ end
+
+ private
+
+ def cluster
+ @cluster ||= project.cluster.present(current_user: current_user)
+ end
+
+ def create_params
+ params.require(:cluster).permit(
+ :gcp_project_id,
+ :gcp_cluster_zone,
+ :gcp_cluster_name,
+ :gcp_cluster_size,
+ :gcp_machine_type,
+ :project_namespace,
+ :enabled)
+ end
+
+ def update_params
+ params.require(:cluster).permit(
+ :project_namespace,
+ :enabled)
+ end
+
+ def authorize_google_api
+ unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
+ .validate_token(expires_at_in_session)
+ redirect_to action: 'login'
+ end
+ end
+
+ def token_in_session
+ @token_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token]
+ end
+
+ def expires_at_in_session
+ @expires_at_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
+ end
+
+ def generate_session_key_redirect(uri)
+ GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
+ session[key] = uri
+ end
+ end
+
+ def authorize_update_cluster!
+ access_denied! unless can?(current_user, :update_cluster, cluster)
+ end
+
+ def authorize_admin_cluster!
+ access_denied! unless can?(current_user, :admin_cluster, cluster)
+ end
+end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 7d0e2b3e2ef..95d7a02e9e9 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -9,6 +9,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
alias_method :user, :actor
+ alias_method :authenticated_user, :actor
# Git clients will not know what authenticity token to send along
skip_before_action :verify_authenticity_token
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index ee6e6f80cdd..b7a108a0ebd 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -278,6 +278,7 @@ class Projects::IssuesController < Projects::ApplicationController
state_event
task_num
lock_version
+ discussion_locked
] + [{ label_ids: [], assignee_ids: [] }]
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 96abdac91b6..1b985ea9763 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -11,7 +11,7 @@ class Projects::JobsController < Projects::ApplicationController
def index
@scope = params[:scope]
@all_builds = project.builds.relevant
- @builds = @all_builds.order('created_at DESC')
+ @builds = @all_builds.order('ci_builds.id DESC')
@builds =
case @scope
when 'pending'
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index 1b0d3aab3fa..536f908d2c5 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -2,6 +2,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
include LfsRequest
skip_before_action :lfs_check_access!, only: [:deprecated]
+ before_action :lfs_check_batch_operation!, only: [:batch]
def batch
unless objects.present?
@@ -90,4 +91,21 @@ class Projects::LfsApiController < Projects::GitHttpClientController
}
}
end
+
+ def lfs_check_batch_operation!
+ if upload_request? && Gitlab::Database.read_only?
+ render(
+ json: {
+ message: lfs_read_only_message
+ },
+ content_type: 'application/vnd.git-lfs+json',
+ status: 403
+ )
+ end
+ end
+
+ # Overridden in EE
+ def lfs_read_only_message
+ _('You cannot write to this read-only GitLab instance.')
+ end
end
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 6602b204fcb..0e71977a58a 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -13,7 +13,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
# Make sure merge requests created before 8.0
# have head file in refs/merge-requests/
def ensure_ref_fetched
- @merge_request.ensure_ref_fetched
+ @merge_request.ensure_ref_fetched if Gitlab::Database.read_write?
end
def merge_request_params
@@ -34,6 +34,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:target_project_id,
:task_num,
:title,
+ :discussion_locked,
label_ids: []
]
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 1096afbb798..99dc3dda9e7 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -120,10 +120,13 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
end
def selected_target_project
- if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil?
+ if @project.id.to_s == params[:target_project_id] || !@project.forked?
@project
+ elsif params[:target_project_id].present?
+ MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project)
+ .execute.find(params[:target_project_id])
else
- @project.forked_project_link.forked_from_project
+ @project.forked_from_project
end
end
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 41a13f6f577..ef7d047b1ad 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -66,7 +66,16 @@ class Projects::NotesController < Projects::ApplicationController
params.merge(last_fetched_at: last_fetched_at)
end
+ def authorize_admin_note!
+ return access_denied! unless can?(current_user, :admin_note, note)
+ end
+
def authorize_resolve_note!
return access_denied! unless can?(current_user, :resolve_note, note)
end
+
+ def authorize_create_note!
+ return unless noteable.lockable?
+ access_denied! unless can?(current_user, :create_note, noteable)
+ end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 5ea3a5d5562..d9142311b6f 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -25,18 +25,33 @@ class RegistrationsController < Devise::RegistrationsController
end
def destroy
- current_user.delete_async(deleted_by: current_user)
-
- respond_to do |format|
- format.html do
- session.try(:destroy)
- redirect_to new_user_session_path, status: 302, notice: "Account scheduled for removal."
- end
+ if destroy_confirmation_valid?
+ current_user.delete_async(deleted_by: current_user)
+ session.try(:destroy)
+ redirect_to new_user_session_path, status: 303, notice: s_('Profiles|Account scheduled for removal.')
+ else
+ redirect_to profile_account_path, status: 303, alert: destroy_confirmation_failure_message
end
end
protected
+ def destroy_confirmation_valid?
+ if current_user.confirm_deletion_with_password?
+ current_user.valid_password?(params[:password])
+ else
+ current_user.username == params[:username]
+ end
+ end
+
+ def destroy_confirmation_failure_message
+ if current_user.confirm_deletion_with_password?
+ s_('Profiles|Invalid password')
+ else
+ s_('Profiles|Invalid username')
+ end
+ end
+
def build_resource(hash = nil)
super
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 4223c6171a6..c01be42c3ee 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -8,12 +8,11 @@ class SessionsController < Devise::SessionsController
prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create]
- prepend_before_action :store_redirect_path, only: [:new]
-
+ prepend_before_action :store_redirect_uri, only: [:new]
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
- after_action :log_failed_login, only: [:new]
+ after_action :log_failed_login, only: [:new], if: :failed_login?
def new
set_minimum_password_length
@@ -46,8 +45,6 @@ class SessionsController < Devise::SessionsController
private
def log_failed_login
- return unless failed_login?
-
Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}")
end
@@ -88,28 +85,36 @@ class SessionsController < Devise::SessionsController
end
end
- def store_redirect_path
- redirect_path =
+ def stored_redirect_uri
+ @redirect_to ||= stored_location_for(:redirect)
+ end
+
+ def store_redirect_uri
+ redirect_uri =
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
- referer_uri = URI(request.referer)
- if referer_uri.host == Gitlab.config.gitlab.host
- referer_uri.request_uri
- else
- request.fullpath
- end
+ URI(request.referer)
else
- request.fullpath
+ URI(request.url)
end
# Prevent a 'you are already signed in' message directly after signing:
# we should never redirect to '/users/sign_in' after signing in successfully.
- unless URI(redirect_path).path == new_user_session_path
- store_location_for(:redirect, redirect_path)
- end
+ return true if redirect_uri.path == new_user_session_path
+
+ redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri)
+
+ @redirect_to = redirect_to
+ store_location_for(:redirect, redirect_to)
+ end
+
+ # Overridden in EE
+ def redirect_allowed_to?(uri)
+ uri.host == Gitlab.config.gitlab.host &&
+ uri.port == Gitlab.config.gitlab.port
end
def two_factor_enabled?
- find_user.try(:two_factor_enabled?)
+ find_user&.two_factor_enabled?
end
def auto_sign_in_with_provider
diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb
new file mode 100644
index 00000000000..189eb3847eb
--- /dev/null
+++ b/app/finders/merge_request_target_project_finder.rb
@@ -0,0 +1,18 @@
+class MergeRequestTargetProjectFinder
+ attr_reader :current_user, :source_project
+
+ def initialize(current_user: nil, source_project:)
+ @current_user = current_user
+ @source_project = source_project
+ end
+
+ def execute
+ if @source_project.fork_network
+ @source_project.fork_network.projects
+ .public_or_visible_to_user(current_user)
+ .with_feature_available_for_user(:merge_requests, current_user)
+ else
+ Project.where(id: source_project)
+ end
+ end
+end
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index 2c28dd81c87..8bf96c0905f 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -4,8 +4,8 @@ module CompareHelper
to.present? &&
from != to &&
can?(current_user, :create_merge_request, project) &&
- project.repository.branch_names.include?(from) &&
- project.repository.branch_names.include?(to)
+ project.repository.branch_exists?(from) &&
+ project.repository.branch_exists?(to)
end
def create_mr_path(from = params[:from], to = params[:to], project = @project)
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 82bceddf1f0..676c1d1988b 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -7,7 +7,12 @@ module GroupsHelper
can?(current_user, :change_share_with_group_lock, group)
end
- def group_icon(group)
+ def group_icon(group, options = {})
+ img_path = group_icon_url(group, options)
+ image_tag img_path, options
+ end
+
+ def group_icon_url(group, options = {})
if group.is_a?(String)
group = Group.find_by_full_path(group)
end
@@ -89,7 +94,7 @@ module GroupsHelper
link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
output =
if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
- image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15)
+ group_icon(group, class: "avatar-tile", width: 15, height: 15)
else
""
end
diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb
index 2c5619ac41b..603b9438e35 100644
--- a/app/helpers/lazy_image_tag_helper.rb
+++ b/app/helpers/lazy_image_tag_helper.rb
@@ -10,6 +10,7 @@ module LazyImageTagHelper
unless options.delete(:lazy) == false
options[:data] ||= {}
options[:data][:src] = path_to_image(source)
+
options[:class] ||= ""
options[:class] << " lazy"
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index c31023f2d9a..5b2c58d193d 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -73,7 +73,8 @@ module MergeRequestsHelper
end
def target_projects(project)
- [project, project.default_merge_request_target].uniq
+ MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: project)
+ .execute
end
def merge_request_button_visibility(merge_request, closed)
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index ce028195e51..c219aa3d6a9 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -130,8 +130,12 @@ module NotesHelper
end
def can_create_note?
+ issuable = @issue || @merge_request
+
if @snippet.is_a?(PersonalSnippet)
can?(current_user, :comment_personal_snippet, @snippet)
+ elsif issuable
+ can?(current_user, :create_note, issuable)
else
can?(current_user, :create_note, @project)
end
diff --git a/app/helpers/numbers_helper.rb b/app/helpers/numbers_helper.rb
new file mode 100644
index 00000000000..45bd3606076
--- /dev/null
+++ b/app/helpers/numbers_helper.rb
@@ -0,0 +1,11 @@
+module NumbersHelper
+ def limited_counter_with_delimiter(resource, **options)
+ limit = options.fetch(:limit, 1000).to_i
+ count = resource.limit(limit + 1).count(:all)
+ if count > limit
+ number_with_delimiter(count - 1, options) + '+'
+ else
+ number_with_delimiter(count, options)
+ end
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 4c0cce54527..20e050195ea 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -293,6 +293,7 @@ module ProjectsHelper
snippets: :read_project_snippet,
settings: :admin_project,
builds: :read_build,
+ clusters: :read_cluster,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index d7eaf6ce24d..00fe67d6ffb 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -19,7 +19,9 @@ module SystemNoteHelper
'discussion' => 'comment',
'moved' => 'arrow-right',
'outdated' => 'pencil',
- 'duplicate' => 'issue-duplicate'
+ 'duplicate' => 'issue-duplicate',
+ 'locked' => 'lock',
+ 'unlocked' => 'lock-open'
}.freeze
def system_note_icon_name(note)
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index c401030e34a..4f5edeb9bda 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -7,12 +7,6 @@ module Emails
mail(to: @user.notification_email, subject: subject("Account was created for you"))
end
- def new_email_email(email_id)
- @email = Email.find(email_id)
- @current_user = @user = @email.user
- mail(to: @user.notification_email, subject: subject("Email was added to your account"))
- end
-
def new_ssh_key_email(key_id)
@key = Key.find_by(id: key_id)
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb
index b35febc9ac5..8b66531ec7b 100644
--- a/app/models/ci/artifact_blob.rb
+++ b/app/models/ci/artifact_blob.rb
@@ -2,6 +2,8 @@ module Ci
class ArtifactBlob
include BlobLike
+ EXTENTIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json].freeze
+
attr_reader :entry
def initialize(entry)
@@ -17,6 +19,7 @@ module Ci
def size
entry.metadata[:size]
end
+ alias_method :external_size, :size
def data
"Build artifact #{path}"
@@ -30,6 +33,27 @@ module Ci
:build_artifact
end
- alias_method :external_size, :size
+ def external_url(project, job)
+ return unless external_link?(job)
+
+ components = project.full_path_components
+ components << "-/jobs/#{job.id}/artifacts/file/#{path}"
+ artifact_path = components[1..-1].join('/')
+
+ "#{pages_config.protocol}://#{components[0]}.#{pages_config.host}/#{artifact_path}"
+ end
+
+ def external_link?(job)
+ pages_config.enabled &&
+ pages_config.artifacts_server &&
+ EXTENTIONS_SERVED_BY_PAGES.include?(File.extname(name)) &&
+ job.project.public?
+ end
+
+ private
+
+ def pages_config
+ Gitlab.config.pages
+ end
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index dd315866e60..6ca46ae89c1 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -11,6 +11,7 @@ module Ci
has_many :deployments, as: :deployable
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
+ has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
@@ -265,6 +266,10 @@ module Ci
update_attributes(coverage: coverage) if coverage.present?
end
+ def parse_trace_sections!
+ ExtractSectionsFromBuildTraceService.new(project, user).execute(self)
+ end
+
def trace
Gitlab::Ci::Trace.new(self)
end
diff --git a/app/models/ci/build_trace_section.rb b/app/models/ci/build_trace_section.rb
new file mode 100644
index 00000000000..ccdb95546c8
--- /dev/null
+++ b/app/models/ci/build_trace_section.rb
@@ -0,0 +1,11 @@
+module Ci
+ class BuildTraceSection < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+
+ belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :project
+ belongs_to :section_name, class_name: 'Ci::BuildTraceSectionName'
+
+ validates :section_name, :build, :project, presence: true, allow_blank: false
+ end
+end
diff --git a/app/models/ci/build_trace_section_name.rb b/app/models/ci/build_trace_section_name.rb
new file mode 100644
index 00000000000..0fdcb1ea329
--- /dev/null
+++ b/app/models/ci/build_trace_section_name.rb
@@ -0,0 +1,11 @@
+module Ci
+ class BuildTraceSectionName < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+
+ belongs_to :project
+ has_many :trace_sections, class_name: 'Ci::BuildTraceSection', foreign_key: :section_name_id
+
+ validates :name, :project, presence: true, allow_blank: false
+ validates :name, uniqueness: { scope: :project_id }
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 3d5acc00f8f..cf3ce3c9e54 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -5,6 +5,7 @@ module Ci
include Importable
include AfterCommitQueue
include Presentable
+ include Gitlab::OptimisticLocking
belongs_to :project
belongs_to :user
@@ -58,6 +59,11 @@ module Ci
auto_devops_source: 2
}
+ enum failure_reason: {
+ unknown_failure: 0,
+ config_error: 1
+ }
+
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
@@ -109,6 +115,12 @@ module Ci
pipeline.auto_canceled_by = nil
end
+ before_transition any => :failed do |pipeline, transition|
+ transition.args.first.try do |reason|
+ pipeline.failure_reason = reason
+ end
+ end
+
after_transition [:created, :pending] => :running do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
@@ -263,7 +275,7 @@ module Ci
end
def cancel_running
- Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable|
+ retry_optimistic_lock(cancelable_statuses) do |cancelable|
cancelable.find_each do |job|
yield(job) if block_given?
job.cancel
@@ -312,6 +324,10 @@ module Ci
@stage_seeds ||= config_processor.stage_seeds(self)
end
+ def seeds_size
+ @seeds_size ||= stage_seeds.sum(&:size)
+ end
+
def has_kubernetes_active?
project.kubernetes_service&.active?
end
@@ -403,7 +419,7 @@ module Ci
end
def update_status
- Gitlab::OptimisticLocking.retry_lock(self) do
+ retry_optimistic_lock(self) do
case latest_builds_status
when 'pending' then enqueue
when 'running' then run
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 8fbfed11bdf..2ec70203710 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -11,7 +11,7 @@ module Avatarable
# If asset_host is set then it is expected that assets are handled by a standalone host.
# That means we do not want to get GitLab's relative_url_root option anymore.
- host = asset_host.present? ? asset_host : gitlab_host
+ host = (asset_host.present? && (!respond_to?(:public?) || public?)) ? asset_host : gitlab_host
[host, avatar.url].join
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 193e459977a..9417033d1f6 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -59,7 +59,7 @@ module CacheMarkdownField
# Update every column in a row if any one is invalidated, as we only store
# one version per row
- def refresh_markdown_cache!(do_update: false)
+ def refresh_markdown_cache
options = { skip_project_check: skip_project_check? }
updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
@@ -71,8 +71,14 @@ module CacheMarkdownField
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
updates.each {|html_field, data| write_attribute(html_field, data) }
+ end
+
+ def refresh_markdown_cache!
+ updates = refresh_markdown_cache
+
+ return unless persisted? && Gitlab::Database.read_write?
- update_columns(updates) if persisted? && do_update
+ update_columns(updates)
end
def cached_html_up_to_date?(markdown_field)
@@ -124,8 +130,8 @@ module CacheMarkdownField
end
# Using before_update here conflicts with elasticsearch-model somehow
- before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache?
- before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache?
+ before_create :refresh_markdown_cache, if: :invalidated_markdown_cache?
+ before_update :refresh_markdown_cache, if: :invalidated_markdown_cache?
end
class_methods do
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index eee1a36ac6b..f5cbb3becad 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -28,6 +28,10 @@ module DiscussionOnDiff
true
end
+ def file_new_path
+ first_note.position.new_path
+ end
+
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 3803e18a96e..7c3ed96bc28 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -81,6 +81,7 @@ module HasStatus
scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') }
scope :manual, -> { where(status: 'manual') }
+ scope :alive, -> { where(status: [:created, :pending, :running]) }
scope :created_or_pending, -> { where(status: [:created, :pending]) }
scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) }
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index fc30d008dea..27f4dedffd3 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -256,23 +256,22 @@ module Issuable
participants(user).include?(user)
end
- def to_hook_data(user)
- hook_data = {
- object_kind: self.class.name.underscore,
- user: user.hook_attrs,
- project: project.hook_attrs,
- object_attributes: hook_attrs,
- labels: labels.map(&:hook_attrs),
- # DEPRECATED
- repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
- }
- if self.is_a?(Issue)
- hook_data[:assignees] = assignees.map(&:hook_attrs) if assignees.any?
- else
- hook_data[:assignee] = assignee.hook_attrs if assignee
+ def to_hook_data(user, old_labels: [], old_assignees: [])
+ changes = previous_changes
+
+ if old_labels != labels
+ changes[:labels] = [old_labels.map(&:hook_attrs), labels.map(&:hook_attrs)]
+ end
+
+ if old_assignees != assignees
+ if self.is_a?(Issue)
+ changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
+ else
+ changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs]
+ end
end
- hook_data
+ Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)
end
def labels_array
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 1c4ddabcad5..5d75b2aa6a3 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -74,4 +74,8 @@ module Noteable
def discussions_can_be_resolved_by?(user)
discussions_to_be_resolved.all? { |discussion| discussion.can_resolve?(user) }
end
+
+ def lockable?
+ [MergeRequest, Issue].include?(self.class)
+ end
end
diff --git a/app/models/concerns/repository_mirroring.rb b/app/models/concerns/repository_mirroring.rb
index fed336c29d6..f6aba91bc4c 100644
--- a/app/models/concerns/repository_mirroring.rb
+++ b/app/models/concerns/repository_mirroring.rb
@@ -1,11 +1,26 @@
module RepositoryMirroring
- def set_remote_as_mirror(name)
- config = raw_repository.rugged.config
+ IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze
+ IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze
+ def set_remote_as_mirror(name)
# This is used to define repository as equivalent as "git clone --mirror"
- config["remote.#{name}.fetch"] = 'refs/*:refs/*'
- config["remote.#{name}.mirror"] = true
- config["remote.#{name}.prune"] = true
+ raw_repository.rugged.config["remote.#{name}.fetch"] = 'refs/*:refs/*'
+ raw_repository.rugged.config["remote.#{name}.mirror"] = true
+ raw_repository.rugged.config["remote.#{name}.prune"] = true
+ end
+
+ def set_import_remote_as_mirror(remote_name)
+ # Add first fetch with Rugged so it does not create its own.
+ raw_repository.rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS
+
+ add_remote_fetch_config(remote_name, IMPORT_TAG_REFS)
+
+ raw_repository.rugged.config["remote.#{remote_name}.mirror"] = true
+ raw_repository.rugged.config["remote.#{remote_name}.prune"] = true
+ end
+
+ def add_remote_fetch_config(remote_name, refspec)
+ run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
end
def fetch_mirror(remote, url)
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index f5048d17d80..22fde2eb134 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -106,6 +106,10 @@ module Routable
RequestStore[full_path_key] ||= uncached_full_path
end
+ def full_path_components
+ full_path.split('/')
+ end
+
def expires_full_path_cache
RequestStore.delete(full_path_key) if RequestStore.active?
@full_path = nil
@@ -152,6 +156,8 @@ module Routable
end
def update_route
+ return if Gitlab::Database.read_only?
+
prepare_route
route.save
end
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index a7d5de48c66..ec3543f7053 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -43,15 +43,17 @@ module TokenAuthenticatable
write_attribute(token_field, token) if token
end
+ # Returns a token, but only saves when the database is in read & write mode
define_method("ensure_#{token_field}!") do
send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend
read_attribute(token_field)
end
+ # Resets the token, but only saves when the database is in read & write mode
define_method("reset_#{token_field}!") do
write_new_token(token_field)
- save!
+ save! if Gitlab::Database.read_write?
end
end
end
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index 07c4846e2ac..6eba87da1a1 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -11,6 +11,8 @@ class DiffDiscussion < Discussion
delegate :position,
:original_position,
:change_position,
+ :on_text?,
+ :on_image?,
to: :first_note
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index e9a60e6ce09..d88a92dc027 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -12,8 +12,8 @@ class DiffNote < Note
validates :original_position, presence: true
validates :position, presence: true
- validates :diff_line, presence: true
- validates :line_code, presence: true, line_code: true
+ validates :diff_line, presence: true, if: :on_text?
+ validates :line_code, presence: true, line_code: true, if: :on_text?
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete
validate :verify_supported
@@ -43,6 +43,14 @@ class DiffNote < Note
end
end
+ def on_text?
+ position.position_type == "text"
+ end
+
+ def on_image?
+ position.position_type == "image"
+ end
+
def diff_file
@diff_file ||= self.original_position.diff_file(self.project.repository)
end
@@ -56,6 +64,8 @@ class DiffNote < Note
end
def original_line_code
+ return unless on_text?
+
self.diff_file.line_code(self.diff_line)
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index b80da7b246a..437df923d2d 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -66,6 +66,10 @@ class Discussion
@context_noteable = context_noteable
end
+ def on_image?
+ false
+ end
+
def ==(other)
other.class == self.class &&
other.context_noteable == self.context_noteable &&
diff --git a/app/models/email.rb b/app/models/email.rb
index 826d4f16edb..384f38f2db7 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -7,6 +7,13 @@ class Email < ActiveRecord::Base
validates :email, presence: true, uniqueness: true, email: true
validate :unique_email, if: ->(email) { email.email_changed? }
+ scope :confirmed, -> { where.not(confirmed_at: nil) }
+
+ after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') }
+
+ devise :confirmable
+ self.reconfirmable = false # currently email can't be changed, no need to reconfirm
+
def email=(value)
write_attribute(:email, value.downcase.strip)
end
@@ -14,4 +21,9 @@ class Email < ActiveRecord::Base
def unique_email
self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
end
+
+ # once email is confirmed, update the gpg signatures
+ def update_invalid_gpg_signatures
+ user.update_invalid_gpg_signatures if confirmed?
+ end
end
diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb
new file mode 100644
index 00000000000..218e37a5312
--- /dev/null
+++ b/app/models/fork_network.rb
@@ -0,0 +1,15 @@
+class ForkNetwork < ActiveRecord::Base
+ belongs_to :root_project, class_name: 'Project'
+ has_many :fork_network_members
+ has_many :projects, through: :fork_network_members
+
+ after_create :add_root_as_member, if: :root_project
+
+ def add_root_as_member
+ projects << root_project
+ end
+
+ def find_forks_in(other_projects)
+ projects.where(id: other_projects)
+ end
+end
diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb
new file mode 100644
index 00000000000..6a9b52a1ef8
--- /dev/null
+++ b/app/models/fork_network_member.rb
@@ -0,0 +1,7 @@
+class ForkNetworkMember < ActiveRecord::Base
+ belongs_to :fork_network
+ belongs_to :project
+ belongs_to :forked_from_project, class_name: 'Project'
+
+ validates :fork_network, :project, presence: true
+end
diff --git a/app/models/gcp/cluster.rb b/app/models/gcp/cluster.rb
new file mode 100644
index 00000000000..18bd6a6dcb4
--- /dev/null
+++ b/app/models/gcp/cluster.rb
@@ -0,0 +1,113 @@
+module Gcp
+ class Cluster < ActiveRecord::Base
+ extend Gitlab::Gcp::Model
+ include Presentable
+
+ belongs_to :project, inverse_of: :cluster
+ belongs_to :user
+ belongs_to :service
+
+ default_value_for :gcp_cluster_zone, 'us-central1-a'
+ default_value_for :gcp_cluster_size, 3
+ default_value_for :gcp_machine_type, 'n1-standard-4'
+
+ attr_encrypted :password,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ attr_encrypted :kubernetes_token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ attr_encrypted :gcp_token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ validates :gcp_project_id,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ validates :gcp_cluster_name,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ validates :gcp_cluster_zone, presence: true
+
+ validates :gcp_cluster_size,
+ presence: true,
+ numericality: {
+ only_integer: true,
+ greater_than: 0
+ }
+
+ validates :project_namespace,
+ allow_blank: true,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ # if we do not do status transition we prevent change
+ validate :restrict_modification, on: :update, unless: :status_changed?
+
+ state_machine :status, initial: :scheduled do
+ state :scheduled, value: 1
+ state :creating, value: 2
+ state :created, value: 3
+ state :errored, value: 4
+
+ event :make_creating do
+ transition any - [:creating] => :creating
+ end
+
+ event :make_created do
+ transition any - [:created] => :created
+ end
+
+ event :make_errored do
+ transition any - [:errored] => :errored
+ end
+
+ before_transition any => [:errored, :created] do |cluster|
+ cluster.gcp_token = nil
+ cluster.gcp_operation_id = nil
+ end
+
+ before_transition any => [:errored] do |cluster, transition|
+ status_reason = transition.args.first
+ cluster.status_reason = status_reason if status_reason
+ end
+ end
+
+ def project_namespace_placeholder
+ "#{project.path}-#{project.id}"
+ end
+
+ def on_creation?
+ scheduled? || creating?
+ end
+
+ def api_url
+ 'https://' + endpoint if endpoint
+ end
+
+ def restrict_modification
+ if on_creation?
+ errors.add(:base, "cannot modify during creation")
+ return false
+ end
+
+ true
+ end
+ end
+end
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index 54bd5b68777..44eda741679 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -9,6 +9,9 @@ class GpgKey < ActiveRecord::Base
belongs_to :user
has_many :gpg_signatures
+ has_many :subkeys, class_name: 'GpgKeySubkey'
+
+ scope :with_subkeys, -> { includes(:subkeys) }
validates :user, presence: true
@@ -36,10 +39,12 @@ class GpgKey < ActiveRecord::Base
before_validation :extract_fingerprint, :extract_primary_keyid
after_commit :update_invalid_gpg_signatures, on: :create
+ after_create :generate_subkeys
def primary_keyid
super&.upcase
end
+ alias_method :keyid, :primary_keyid
def fingerprint
super&.upcase
@@ -49,6 +54,10 @@ class GpgKey < ActiveRecord::Base
super(value&.strip)
end
+ def keyids
+ [keyid].concat(subkeys.map(&:keyid))
+ end
+
def user_infos
@user_infos ||= Gitlab::Gpg.user_infos_from_key(key)
end
@@ -82,10 +91,11 @@ class GpgKey < ActiveRecord::Base
def revoke
GpgSignature
- .where(gpg_key: self)
+ .with_key_and_subkeys(self)
.where.not(verification_status: GpgSignature.verification_statuses[:unknown_key])
.update_all(
gpg_key_id: nil,
+ gpg_key_subkey_id: nil,
verification_status: GpgSignature.verification_statuses[:unknown_key],
updated_at: Time.zone.now
)
@@ -106,4 +116,12 @@ class GpgKey < ActiveRecord::Base
# only allows one key
self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first
end
+
+ def generate_subkeys
+ gpg_subkeys = Gitlab::Gpg.subkeys_from_key(key)
+
+ gpg_subkeys[primary_keyid]&.each do |subkey_data|
+ subkeys.create!(keyid: subkey_data[:keyid], fingerprint: subkey_data[:fingerprint])
+ end
+ end
end
diff --git a/app/models/gpg_key_subkey.rb b/app/models/gpg_key_subkey.rb
new file mode 100644
index 00000000000..b57922aba30
--- /dev/null
+++ b/app/models/gpg_key_subkey.rb
@@ -0,0 +1,22 @@
+class GpgKeySubkey < ActiveRecord::Base
+ include ShaAttribute
+
+ sha_attribute :keyid
+ sha_attribute :fingerprint
+
+ belongs_to :gpg_key
+
+ validates :gpg_key_id, presence: true
+ validates :fingerprint, :keyid, presence: true, uniqueness: true
+
+ delegate :key, :user, :user_infos, :verified?, :verified_user_infos,
+ :verified_and_belongs_to_email?, to: :gpg_key
+
+ def keyid
+ super&.upcase
+ end
+
+ def fingerprint
+ super&.upcase
+ end
+end
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
index 1f047a32c84..bf88d75246f 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -15,11 +15,42 @@ class GpgSignature < ActiveRecord::Base
belongs_to :project
belongs_to :gpg_key
+ belongs_to :gpg_key_subkey
validates :commit_sha, presence: true
validates :project_id, presence: true
validates :gpg_key_primary_keyid, presence: true
+ def self.with_key_and_subkeys(gpg_key)
+ subkey_ids = gpg_key.subkeys.pluck(:id)
+
+ where(
+ arel_table[:gpg_key_id].eq(gpg_key.id).or(
+ arel_table[:gpg_key_subkey_id].in(subkey_ids)
+ )
+ )
+ end
+
+ def gpg_key=(model)
+ case model
+ when GpgKey
+ super
+ when GpgKeySubkey
+ self.gpg_key_subkey = model
+ when NilClass
+ super
+ self.gpg_key_subkey = nil
+ end
+ end
+
+ def gpg_key
+ if gpg_key_id
+ super
+ elsif gpg_key_subkey_id
+ gpg_key_subkey
+ end
+ end
+
def gpg_key_primary_keyid
super&.upcase
end
@@ -29,6 +60,8 @@ class GpgSignature < ActiveRecord::Base
end
def gpg_commit
+ return unless commit
+
Gitlab::Gpg::Commit.new(commit)
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 155c5d972b7..36e4108b9d6 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -74,20 +74,6 @@ class Issue < ActiveRecord::Base
end
end
- def hook_attrs
- assignee_ids = self.assignee_ids
-
- attrs = {
- total_time_spent: total_time_spent,
- human_total_time_spent: human_total_time_spent,
- human_time_estimate: human_time_estimate,
- assignee_ids: assignee_ids,
- assignee_id: assignee_ids.first # This key is deprecated
- }
-
- attributes.merge!(attrs)
- end
-
def self.reference_prefix
'#'
end
@@ -131,6 +117,10 @@ class Issue < ActiveRecord::Base
"id DESC")
end
+ def hook_attrs
+ Gitlab::HookData::IssueBuilder.new(self).build
+ end
+
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
diff --git a/app/models/legacy_diff_discussion.rb b/app/models/legacy_diff_discussion.rb
index 3c1d34db5fa..80fc6304fd4 100644
--- a/app/models/legacy_diff_discussion.rb
+++ b/app/models/legacy_diff_discussion.rb
@@ -17,6 +17,14 @@ class LegacyDiffDiscussion < Discussion
true
end
+ def on_image?
+ false
+ end
+
+ def on_text?
+ true
+ end
+
def active?(*args)
return @active if @active.present?
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 0ba00d447e8..972a35dde4d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -179,6 +179,10 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
end
+ def hook_attrs
+ Gitlab::HookData::MergeRequestBuilder.new(self).build
+ end
+
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
@@ -403,7 +407,7 @@ class MergeRequest < ActiveRecord::Base
return false unless for_fork?
return true unless source_project
- !source_project.forked_from?(target_project)
+ !source_project.in_fork_network_of?(target_project)
end
def reopenable?
@@ -415,6 +419,8 @@ class MergeRequest < ActiveRecord::Base
end
def create_merge_request_diff
+ fetch_ref
+
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
Gitlab::GitalyClient.allow_n_plus_1_calls do
merge_request_diffs.create
@@ -462,6 +468,7 @@ class MergeRequest < ActiveRecord::Base
return unless open?
old_diff_refs = self.diff_refs
+
create_merge_request_diff
MergeRequests::MergeRequestDiffCacheService.new.execute(self)
new_diff_refs = self.diff_refs
@@ -474,7 +481,7 @@ class MergeRequest < ActiveRecord::Base
end
def check_if_can_be_merged
- return unless unchecked?
+ return unless unchecked? && Gitlab::Database.read_write?
can_be_merged =
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
@@ -584,24 +591,6 @@ class MergeRequest < ActiveRecord::Base
!discussions_to_be_resolved?
end
- def hook_attrs
- attrs = {
- source: source_project.try(:hook_attrs),
- target: target_project.hook_attrs,
- last_commit: nil,
- work_in_progress: work_in_progress?,
- total_time_spent: total_time_spent,
- human_total_time_spent: human_total_time_spent,
- human_time_estimate: human_time_estimate
- }
-
- if diff_head_commit
- attrs[:last_commit] = diff_head_commit.hook_attrs
- end
-
- attributes.merge!(attrs)
- end
-
def for_fork?
target_project != source_project
end
@@ -686,13 +675,13 @@ class MergeRequest < ActiveRecord::Base
def source_branch_exists?
return false unless self.source_project
- self.source_project.repository.branch_names.include?(self.source_branch)
+ self.source_project.repository.branch_exists?(self.source_branch)
end
def target_branch_exists?
return false unless self.target_project
- self.target_project.repository.branch_names.include?(self.target_branch)
+ self.target_project.repository.branch_exists?(self.target_branch)
end
def merge_commit_message(include_description: false)
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 58050e1f438..faf0b95f842 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -55,7 +55,6 @@ class MergeRequestDiff < ActiveRecord::Base
end
def ensure_commit_shas
- merge_request.fetch_ref
self.start_commit_sha ||= merge_request.target_branch_sha
self.head_commit_sha ||= merge_request.source_branch_sha
self.base_commit_sha ||= find_base_sha
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index a6f517ce2ce..0601a61a926 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -139,7 +139,9 @@ class Namespace < ActiveRecord::Base
end
def find_fork_of(project)
- projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id)
+ return nil unless project.fork_network
+
+ project.fork_network.find_forks_in(projects).first
end
def lfs_enabled?
diff --git a/app/models/note.rb b/app/models/note.rb
index f44590e2144..ceded9f2aef 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -134,14 +134,22 @@ class Note < ActiveRecord::Base
Discussion.build(notes)
end
+ # Group diff discussions by line code or file path.
+ # It is not needed to group by line code when comment is
+ # on an image.
def grouped_diff_discussions(diff_refs = nil)
groups = {}
diff_notes.fresh.discussions.each do |discussion|
- line_code = discussion.line_code_in_diffs(diff_refs)
-
- if line_code
- discussions = groups[line_code] ||= []
+ group_key =
+ if discussion.on_image?
+ discussion.file_new_path
+ else
+ discussion.line_code_in_diffs(diff_refs)
+ end
+
+ if group_key
+ discussions = groups[group_key] ||= []
discussions << discussion
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index fdbc1f5d50c..6e1cf9e31ee 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -121,11 +121,20 @@ class Project < ActiveRecord::Base
has_one :mock_monitoring_service
has_one :microsoft_teams_service
+ # TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id"
has_many :forks, through: :forked_project_links, source: :forked_to_project
+ # TODO: replace these relations with the fork network versions
+
+ has_one :root_of_fork_network,
+ foreign_key: 'root_project_id',
+ inverse_of: :root_project,
+ class_name: 'ForkNetwork'
+ has_one :fork_network_member
+ has_one :fork_network, through: :fork_network_member
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
@@ -168,6 +177,7 @@ class Project < ActiveRecord::Base
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
+ has_one :cluster, class_name: 'Gcp::Cluster', inverse_of: :project
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
@@ -182,6 +192,7 @@ class Project < ActiveRecord::Base
# bulk that doesn't involve loading the rows into memory. As a result we're
# still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :runner_projects, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
@@ -823,7 +834,7 @@ class Project < ActiveRecord::Base
end
def cache_has_external_issue_tracker
- update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
+ update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
end
def has_wiki?
@@ -843,7 +854,7 @@ class Project < ActiveRecord::Base
end
def cache_has_external_wiki
- update_column(:has_external_wiki, services.external_wikis.any?)
+ update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
end
def find_or_initialize_services(exceptions: [])
@@ -1008,6 +1019,11 @@ class Project < ActiveRecord::Base
end
def forked?
+ return true if fork_network && fork_network.root_project != self
+
+ # TODO: Use only the above conditional using the `fork_network`
+ # This is the old conditional that looks at the `forked_project_link`, we
+ # fall back to this while we're migrating the new models
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end
@@ -1044,6 +1060,8 @@ class Project < ActiveRecord::Base
end
true
+ rescue GRPC::Internal # if the path is too long
+ false
end
def create_repository(force: false)
@@ -1125,8 +1143,19 @@ class Project < ActiveRecord::Base
end
end
- def forked_from?(project)
- forked? && project == forked_from_project
+ def forked_from?(other_project)
+ forked? && forked_from_project == other_project
+ end
+
+ def in_fork_network_of?(other_project)
+ # TODO: Remove this in a next release when all fork_networks are populated
+ # This makes sure all MergeRequests remain valid while the projects don't
+ # have a fork_network yet.
+ return true if forked_from?(other_project)
+
+ return false if fork_network.nil? || other_project.fork_network.nil?
+
+ fork_network == other_project.fork_network
end
def origin_merge_requests
diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb
index e2ad586aea7..22a65b5145e 100644
--- a/app/models/project_services/chat_message/base_message.rb
+++ b/app/models/project_services/chat_message/base_message.rb
@@ -3,6 +3,7 @@ require 'slack-notifier'
module ChatMessage
class BaseMessage
attr_reader :markdown
+ attr_reader :user_full_name
attr_reader :user_name
attr_reader :user_avatar
attr_reader :project_name
@@ -12,10 +13,19 @@ module ChatMessage
@markdown = params[:markdown] || false
@project_name = params.dig(:project, :path_with_namespace) || params[:project_name]
@project_url = params.dig(:project, :web_url) || params[:project_url]
+ @user_full_name = params.dig(:user, :name) || params[:user_full_name]
@user_name = params.dig(:user, :username) || params[:user_name]
@user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
end
+ def user_combined_name
+ if user_full_name.present?
+ "#{user_full_name} (#{user_name})"
+ else
+ user_name
+ end
+ end
+
def pretext
return message if markdown
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
index 4b9a2b1e1f3..1327b075858 100644
--- a/app/models/project_services/chat_message/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -29,7 +29,7 @@ module ChatMessage
def activity
{
- title: "Issue #{state} by #{user_name}",
+ title: "Issue #{state} by #{user_combined_name}",
subtitle: "in #{project_link}",
text: issue_link,
image: user_avatar
@@ -40,9 +40,9 @@ module ChatMessage
def message
if state == 'opened'
- "[#{project_link}] Issue #{state} by #{user_name}"
+ "[#{project_link}] Issue #{state} by #{user_combined_name}"
else
- "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}"
+ "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
end
end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index 7d0de81cdf0..f412b6833d9 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -24,7 +24,7 @@ module ChatMessage
def activity
{
- title: "Merge Request #{state} by #{user_name}",
+ title: "Merge Request #{state} by #{user_combined_name}",
subtitle: "in #{project_link}",
text: merge_request_link,
image: user_avatar
@@ -46,7 +46,7 @@ module ChatMessage
end
def merge_request_message
- "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}"
+ "#{user_combined_name} #{state} #{merge_request_link} in #{project_link}: #{title}"
end
def merge_request_link
diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb
index 2da4c244229..7f9486132e6 100644
--- a/app/models/project_services/chat_message/note_message.rb
+++ b/app/models/project_services/chat_message/note_message.rb
@@ -32,7 +32,7 @@ module ChatMessage
def activity
{
- title: "#{user_name} #{link('commented on ' + target, note_url)}",
+ title: "#{user_combined_name} #{link('commented on ' + target, note_url)}",
subtitle: "in #{project_link}",
text: formatted_title,
image: user_avatar
@@ -42,7 +42,7 @@ module ChatMessage
private
def message
- "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
+ "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
end
def format_title(title)
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index d63d4ec2b12..2135122278a 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -9,7 +9,7 @@ module ChatMessage
def initialize(data)
super
- @user_name = data.dig(:user, :name) || 'API'
+ @user_name = data.dig(:user, :username) || 'API'
pipeline_attributes = data[:object_attributes]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@@ -35,7 +35,7 @@ module ChatMessage
def activity
{
- title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status}",
+ title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_combined_name} #{humanized_status}",
subtitle: "in #{project_link}",
text: "in #{pretty_duration(duration)}",
image: user_avatar || ''
@@ -45,7 +45,7 @@ module ChatMessage
private
def message
- "#{project_link}: Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status} in #{pretty_duration(duration)}"
+ "#{project_link}: Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_combined_name} #{humanized_status} in #{pretty_duration(duration)}"
end
def humanized_status
diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb
index c52dd6ef8ef..8d599c5f116 100644
--- a/app/models/project_services/chat_message/push_message.rb
+++ b/app/models/project_services/chat_message/push_message.rb
@@ -33,7 +33,7 @@ module ChatMessage
end
{
- title: "#{user_name} #{action} #{ref_type}",
+ title: "#{user_combined_name} #{action} #{ref_type}",
subtitle: "in #{project_link}",
text: compare_link,
image: user_avatar
@@ -57,15 +57,15 @@ module ChatMessage
end
def new_branch_message
- "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}"
+ "#{user_combined_name} pushed new #{ref_type} #{branch_link} to #{project_link}"
end
def removed_branch_message
- "#{user_name} removed #{ref_type} #{ref} from #{project_link}"
+ "#{user_combined_name} removed #{ref_type} #{ref} from #{project_link}"
end
def push_message
- "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})"
+ "#{user_combined_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})"
end
def commit_messages
diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb
index a139a8ee727..d84b80f2de2 100644
--- a/app/models/project_services/chat_message/wiki_page_message.rb
+++ b/app/models/project_services/chat_message/wiki_page_message.rb
@@ -31,7 +31,7 @@ module ChatMessage
def activity
{
- title: "#{user_name} #{action} #{wiki_page_link}",
+ title: "#{user_combined_name} #{action} #{wiki_page_link}",
subtitle: "in #{project_link}",
text: title,
image: user_avatar
@@ -41,7 +41,7 @@ module ChatMessage
private
def message
- "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
+ "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
end
def description_message
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 298569cb7a6..6e311806be1 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -53,13 +53,17 @@ class SentNotification < ActiveRecord::Base
end
def unsubscribable?
- !for_commit?
+ !(for_commit? || for_snippet?)
end
def for_commit?
noteable_type == "Commit"
end
+ def for_snippet?
+ noteable_type.end_with?('Snippet')
+ end
+
def noteable
if for_commit?
project.commit(commit_id) rescue nil
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 0b33e45473b..1f9f8d7286b 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -2,7 +2,7 @@ class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved
- opened closed merged duplicate
+ opened closed merged duplicate locked unlocked
outdated
].freeze
diff --git a/app/models/user.rb b/app/models/user.rb
index 4e71a3e11c2..533a776bc65 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -163,15 +163,16 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
before_validation :set_public_email, if: :public_email_changed?
-
- after_update :update_emails_with_primary_email, if: :email_changed?
before_save :ensure_authentication_token, :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: :external_changed?
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
+ before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
after_save :ensure_namespace_correct
+ after_destroy :post_destroy_hook
+ after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
+
after_initialize :set_projects_limit
- after_destroy :post_destroy_hook
# User's Layout preference
enum layout: [:fixed, :fluid]
@@ -458,6 +459,14 @@ class User < ActiveRecord::Base
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
+ def remember_me!
+ super if ::Gitlab::Database.read_write?
+ end
+
+ def forget_me!
+ super if ::Gitlab::Database.read_write?
+ end
+
def disable_two_factor!
transaction do
update_attributes(
@@ -525,12 +534,24 @@ class User < ActiveRecord::Base
errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end
+ # see if the new email is already a verified secondary email
+ def check_for_verified_email
+ skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
+ end
+
+ # Note: the use of the Emails services will cause `saves` on the user object, running
+ # through the callbacks again and can have side effects, such as the `previous_changes`
+ # hash and `_was` variables getting munged.
+ # By using an `after_commit` instead of `after_update`, we avoid the recursive callback
+ # scenario, though it then requires us to use the `previous_changes` hash
def update_emails_with_primary_email
+ previous_email = previous_changes[:email][0] # grab this before the DestroyService is called
primary_email_record = emails.find_by(email: email)
- if primary_email_record
- Emails::DestroyService.new(self, user: self, email: email).execute
- Emails::CreateService.new(self, user: self, email: email_was).execute
- end
+ Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
+
+ # the original primary email was confirmed, and we want that to carry over. We don't
+ # have access to the original confirmation values at this point, so just set confirmed_at
+ Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
end
def update_invalid_gpg_signatures
@@ -641,6 +662,10 @@ class User < ActiveRecord::Base
Ability.allowed?(self, action, subject)
end
+ def confirm_deletion_with_password?
+ !password_automatically_set? && allow_password_authentication?
+ end
+
def first_name
name.split.first unless name.blank?
end
@@ -680,15 +705,7 @@ class User < ActiveRecord::Base
end
def fork_of(project)
- links = ForkedProjectLink.where(
- forked_from_project_id: project,
- forked_to_project_id: personal_projects.unscope(:order)
- )
- if links.any?
- links.first.forked_to_project
- else
- nil
- end
+ namespace.find_fork_of(project)
end
def ldap_user?
@@ -816,6 +833,10 @@ class User < ActiveRecord::Base
avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
end
+ def primary_email_verified?
+ confirmed? && !temp_oauth_email?
+ end
+
def all_emails
all_emails = []
all_emails << email unless temp_oauth_email?
@@ -823,6 +844,18 @@ class User < ActiveRecord::Base
all_emails
end
+ def verified_emails
+ verified_emails = []
+ verified_emails << email if primary_email_verified?
+ verified_emails.concat(emails.confirmed.pluck(:email))
+ verified_emails
+ end
+
+ def verified_email?(check_email)
+ downcased = check_email.downcase
+ email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
+ end
+
def hook_attrs
{
name: name,
@@ -1047,10 +1080,6 @@ class User < ActiveRecord::Base
ensure_rss_token!
end
- def verified_email?(email)
- self.email == email
- end
-
def sync_attribute?(attribute)
return true if ldap_user? && attribute == :email
diff --git a/app/policies/gcp/cluster_policy.rb b/app/policies/gcp/cluster_policy.rb
new file mode 100644
index 00000000000..e77173ea6e1
--- /dev/null
+++ b/app/policies/gcp/cluster_policy.rb
@@ -0,0 +1,12 @@
+module Gcp
+ class ClusterPolicy < BasePolicy
+ alias_method :cluster, :subject
+
+ delegate { @subject.project }
+
+ rule { can?(:master_access) }.policy do
+ enable :update_cluster
+ enable :admin_cluster
+ end
+ end
+end
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index daf6fa9e18a..f0aa16d2ecf 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -1,6 +1,10 @@
class IssuablePolicy < BasePolicy
delegate { @subject.project }
+ condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? }
+
+ condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) }
+
desc "User is the assignee or author"
condition(:assignee_or_author) do
@user && @subject.assignee_or_author?(@user)
@@ -12,4 +16,12 @@ class IssuablePolicy < BasePolicy
enable :read_merge_request
enable :update_merge_request
end
+
+ rule { locked & ~is_project_member }.policy do
+ prevent :create_note
+ prevent :update_note
+ prevent :admin_note
+ prevent :resolve_note
+ prevent :edit_note
+ end
end
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index 20cd51cfb99..d4cb5a77e63 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -1,5 +1,6 @@
class NotePolicy < BasePolicy
delegate { @subject.project }
+ delegate { @subject.noteable if @subject.noteable.lockable? }
condition(:is_author) { @user && @subject.author == @user }
condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? }
@@ -8,6 +9,7 @@ class NotePolicy < BasePolicy
condition(:editable, scope: :subject) { @subject.editable? }
rule { ~editable | anonymous }.prevent :edit_note
+
rule { is_author | admin }.enable :edit_note
rule { can?(:master_access) }.enable :edit_note
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index b7b5bd34189..f599eab42f2 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -193,6 +193,8 @@ class ProjectPolicy < BasePolicy
enable :admin_pages
enable :read_pages
enable :update_pages
+ enable :read_cluster
+ enable :create_cluster
end
rule { can?(:public_user_access) }.policy do
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index a542bdd8295..099b4720fb6 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -1,7 +1,18 @@
module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated
+ FAILURE_REASONS = {
+ config_error: 'CI/CD YAML configuration error!'
+ }.freeze
+
presents :pipeline
+ def failure_reason
+ return unless pipeline.failure_reason?
+
+ FAILURE_REASONS[pipeline.failure_reason.to_sym] ||
+ pipeline.failure_reason
+ end
+
def status_title
if auto_canceled?
"Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
diff --git a/app/presenters/gcp/cluster_presenter.rb b/app/presenters/gcp/cluster_presenter.rb
new file mode 100644
index 00000000000..f7908f92a37
--- /dev/null
+++ b/app/presenters/gcp/cluster_presenter.rb
@@ -0,0 +1,9 @@
+module Gcp
+ class ClusterPresenter < Gitlab::View::Presenter::Delegated
+ presents :cluster
+
+ def gke_cluster_url
+ "https://console.cloud.google.com/kubernetes/clusters/details/#{gcp_cluster_zone}/#{gcp_cluster_name}"
+ end
+ end
+end
diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb
new file mode 100644
index 00000000000..08a113c4d8a
--- /dev/null
+++ b/app/serializers/cluster_entity.rb
@@ -0,0 +1,6 @@
+class ClusterEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :status_name, as: :status
+ expose :status_reason
+end
diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb
new file mode 100644
index 00000000000..2c87202a105
--- /dev/null
+++ b/app/serializers/cluster_serializer.rb
@@ -0,0 +1,7 @@
+class ClusterSerializer < BaseSerializer
+ entity ClusterEntity
+
+ def represent_status(resource)
+ represent(resource, { only: [:status, :status_reason] })
+ end
+end
diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb
index e4e9d8ef90a..c8dd98cc04d 100644
--- a/app/serializers/commit_entity.rb
+++ b/app/serializers/commit_entity.rb
@@ -1,4 +1,4 @@
-class CommitEntity < API::Entities::RepoCommit
+class CommitEntity < API::Entities::Commit
include RequestAwareEntity
expose :author, using: UserEntity
diff --git a/app/serializers/group_entity.rb b/app/serializers/group_entity.rb
index 7c872a3e986..6d8466da902 100644
--- a/app/serializers/group_entity.rb
+++ b/app/serializers/group_entity.rb
@@ -45,6 +45,6 @@ class GroupEntity < Grape::Entity
end
expose :avatar_url do |group|
- group_icon(group)
+ group_icon_url(group)
end
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 0d6feb78173..10d3ad0214b 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -3,6 +3,7 @@ class IssueEntity < IssuableEntity
expose :branch_name
expose :confidential
+ expose :discussion_locked
expose :assignees, using: API::Entities::UserBasic
expose :due_date
expose :moved_to_id
@@ -14,7 +15,7 @@ class IssueEntity < IssuableEntity
expose :current_user do
expose :can_create_note do |issue|
- can?(request.current_user, :create_note, issue.project)
+ can?(request.current_user, :create_note, issue)
end
expose :can_update do |issue|
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 36537c5bd02..297a459e394 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -42,6 +42,7 @@ class MergeRequestEntity < IssuableEntity
expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
+ expose :mergeable?, as: :mergeable
expose :remove_source_branch?, as: :remove_source_branch
expose :project_archived do |merge_request|
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 357fc71f877..6457294b285 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -20,6 +20,7 @@ class PipelineEntity < Grape::Entity
expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable
+ expose :failure_reason?, as: :failure_reason
end
expose :details do
@@ -44,6 +45,11 @@ class PipelineEntity < Grape::Entity
end
expose :commit, using: CommitEntity
+ expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
+
+ expose :failure_reason, if: -> (pipeline, _) { pipeline.failure_reason? } do |pipeline|
+ pipeline.present.failure_reason
+ end
expose :retry_path, if: -> (*) { can_retry? } do |pipeline|
retry_project_pipeline_path(pipeline.project, pipeline)
@@ -53,8 +59,6 @@ class PipelineEntity < Grape::Entity
cancel_project_pipeline_path(pipeline.project, pipeline)
end
- expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
-
private
alias_method :pipeline, :object
diff --git a/app/services/ci/create_cluster_service.rb b/app/services/ci/create_cluster_service.rb
new file mode 100644
index 00000000000..f7ee0e468e2
--- /dev/null
+++ b/app/services/ci/create_cluster_service.rb
@@ -0,0 +1,15 @@
+module Ci
+ class CreateClusterService < BaseService
+ def execute(access_token)
+ params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE
+
+ cluster_params =
+ params.merge(user: current_user,
+ gcp_token: access_token)
+
+ project.create_cluster(cluster_params).tap do |cluster|
+ ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
+ end
+ end
+ end
+end
diff --git a/app/services/ci/extract_sections_from_build_trace_service.rb b/app/services/ci/extract_sections_from_build_trace_service.rb
new file mode 100644
index 00000000000..75f9e0f897d
--- /dev/null
+++ b/app/services/ci/extract_sections_from_build_trace_service.rb
@@ -0,0 +1,30 @@
+module Ci
+ class ExtractSectionsFromBuildTraceService < BaseService
+ def execute(build)
+ return false unless build.trace_sections.empty?
+
+ Gitlab::Database.bulk_insert(BuildTraceSection.table_name, extract_sections(build))
+ true
+ end
+
+ private
+
+ def find_or_create_name(name)
+ project.build_trace_section_names.find_or_create_by!(name: name)
+ rescue ActiveRecord::RecordInvalid
+ project.build_trace_section_names.find_by!(name: name)
+ end
+
+ def extract_sections(build)
+ build.trace.extract_sections.map do |attr|
+ name = attr.delete(:name)
+ name_record = find_or_create_name(name)
+
+ attr.merge(
+ build_id: build.id,
+ project_id: project.id,
+ section_name_id: name_record.id)
+ end
+ end
+ end
+end
diff --git a/app/services/ci/fetch_gcp_operation_service.rb b/app/services/ci/fetch_gcp_operation_service.rb
new file mode 100644
index 00000000000..0b68e4d6ea9
--- /dev/null
+++ b/app/services/ci/fetch_gcp_operation_service.rb
@@ -0,0 +1,17 @@
+module Ci
+ class FetchGcpOperationService
+ def execute(cluster)
+ api_client =
+ GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
+
+ operation = api_client.projects_zones_operations(
+ cluster.gcp_project_id,
+ cluster.gcp_cluster_zone,
+ cluster.gcp_operation_id)
+
+ yield(operation) if block_given?
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+ end
+end
diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb
new file mode 100644
index 00000000000..44da87cb00c
--- /dev/null
+++ b/app/services/ci/fetch_kubernetes_token_service.rb
@@ -0,0 +1,72 @@
+##
+# TODO:
+# Almost components in this class were copied from app/models/project_services/kubernetes_service.rb
+# We should dry up those classes not to repeat the same code.
+# Maybe we should have a special facility (e.g. lib/kubernetes_api) to maintain all Kubernetes API caller.
+module Ci
+ class FetchKubernetesTokenService
+ attr_reader :api_url, :ca_pem, :username, :password
+
+ def initialize(api_url, ca_pem, username, password)
+ @api_url = api_url
+ @ca_pem = ca_pem
+ @username = username
+ @password = password
+ end
+
+ def execute
+ read_secrets.each do |secret|
+ name = secret.dig('metadata', 'name')
+ if /default-token/ =~ name
+ token_base64 = secret.dig('data', 'token')
+ return Base64.decode64(token_base64) if token_base64
+ end
+ end
+
+ nil
+ end
+
+ private
+
+ def read_secrets
+ kubeclient = build_kubeclient!
+
+ kubeclient.get_secrets.as_json
+ rescue KubeException => err
+ raise err unless err.error_code == 404
+ []
+ end
+
+ def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ raise "Incomplete settings" unless api_url && username && password
+
+ ::Kubeclient::Client.new(
+ join_api_url(api_path),
+ api_version,
+ auth_options: { username: username, password: password },
+ ssl_options: kubeclient_ssl_options,
+ http_proxy_uri: ENV['http_proxy']
+ )
+ end
+
+ def join_api_url(api_path)
+ url = URI.parse(api_url)
+ prefix = url.path.sub(%r{/+\z}, '')
+
+ url.path = [prefix, api_path].join("/")
+
+ url.to_s
+ end
+
+ def kubeclient_ssl_options
+ opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
+
+ if ca_pem.present?
+ opts[:cert_store] = OpenSSL::X509::Store.new
+ opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
+ end
+
+ opts
+ end
+ end
+end
diff --git a/app/services/ci/finalize_cluster_creation_service.rb b/app/services/ci/finalize_cluster_creation_service.rb
new file mode 100644
index 00000000000..347875c5697
--- /dev/null
+++ b/app/services/ci/finalize_cluster_creation_service.rb
@@ -0,0 +1,33 @@
+module Ci
+ class FinalizeClusterCreationService
+ def execute(cluster)
+ api_client =
+ GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
+
+ begin
+ gke_cluster = api_client.projects_zones_clusters_get(
+ cluster.gcp_project_id,
+ cluster.gcp_cluster_zone,
+ cluster.gcp_cluster_name)
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+
+ endpoint = gke_cluster.endpoint
+ api_url = 'https://' + endpoint
+ ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate)
+ username = gke_cluster.master_auth.username
+ password = gke_cluster.master_auth.password
+
+ kubernetes_token = Ci::FetchKubernetesTokenService.new(
+ api_url, ca_cert, username, password).execute
+
+ unless kubernetes_token
+ return cluster.make_errored!('Failed to get a default token of kubernetes')
+ end
+
+ Ci::IntegrateClusterService.new.execute(
+ cluster, endpoint, ca_cert, kubernetes_token, username, password)
+ end
+ end
+end
diff --git a/app/services/ci/integrate_cluster_service.rb b/app/services/ci/integrate_cluster_service.rb
new file mode 100644
index 00000000000..d123ce8d26b
--- /dev/null
+++ b/app/services/ci/integrate_cluster_service.rb
@@ -0,0 +1,26 @@
+module Ci
+ class IntegrateClusterService
+ def execute(cluster, endpoint, ca_cert, token, username, password)
+ Gcp::Cluster.transaction do
+ cluster.update!(
+ enabled: true,
+ endpoint: endpoint,
+ ca_cert: ca_cert,
+ kubernetes_token: token,
+ username: username,
+ password: password,
+ service: cluster.project.find_or_initialize_service('kubernetes'),
+ status_event: :make_created)
+
+ cluster.service.update!(
+ active: true,
+ api_url: cluster.api_url,
+ ca_pem: ca_cert,
+ namespace: cluster.project_namespace,
+ token: token)
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ cluster.make_errored!("Failed to integrate cluster into kubernetes_service: #{e.message}")
+ end
+ end
+end
diff --git a/app/services/ci/provision_cluster_service.rb b/app/services/ci/provision_cluster_service.rb
new file mode 100644
index 00000000000..52d80b01813
--- /dev/null
+++ b/app/services/ci/provision_cluster_service.rb
@@ -0,0 +1,36 @@
+module Ci
+ class ProvisionClusterService
+ def execute(cluster)
+ api_client =
+ GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
+
+ begin
+ operation = api_client.projects_zones_clusters_create(
+ cluster.gcp_project_id,
+ cluster.gcp_cluster_zone,
+ cluster.gcp_cluster_name,
+ cluster.gcp_cluster_size,
+ machine_type: cluster.gcp_machine_type)
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+
+ unless operation.status == 'RUNNING' || operation.status == 'PENDING'
+ return cluster.make_errored!("Operation status is unexpected; #{operation.status_message}")
+ end
+
+ cluster.gcp_operation_id = api_client.parse_operation_id(operation.self_link)
+
+ unless cluster.gcp_operation_id
+ return cluster.make_errored!('Can not find operation_id from self_link')
+ end
+
+ if cluster.make_creating
+ WaitForClusterCreationWorker.perform_in(
+ WaitForClusterCreationWorker::INITIAL_INTERVAL, cluster.id)
+ else
+ return cluster.make_errored!("Failed to update cluster record; #{cluster.errors}")
+ end
+ end
+ end
+end
diff --git a/app/services/ci/update_cluster_service.rb b/app/services/ci/update_cluster_service.rb
new file mode 100644
index 00000000000..70d88fca660
--- /dev/null
+++ b/app/services/ci/update_cluster_service.rb
@@ -0,0 +1,22 @@
+module Ci
+ class UpdateClusterService < BaseService
+ def execute(cluster)
+ Gcp::Cluster.transaction do
+ cluster.update!(params)
+
+ if params['enabled'] == 'true'
+ cluster.service.update!(
+ active: true,
+ api_url: cluster.api_url,
+ ca_pem: cluster.ca_cert,
+ namespace: cluster.project_namespace,
+ token: cluster.kubernetes_token)
+ else
+ cluster.service.update!(active: false)
+ end
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ cluster.errors.add(:base, e.message)
+ end
+ end
+end
diff --git a/app/services/emails/base_service.rb b/app/services/emails/base_service.rb
index 7f591c89411..5bbceeb3b3f 100644
--- a/app/services/emails/base_service.rb
+++ b/app/services/emails/base_service.rb
@@ -1,9 +1,8 @@
module Emails
class BaseService
- def initialize(current_user, opts)
- @current_user = current_user
- @user = opts.delete(:user)
- @email = opts[:email]
+ def initialize(current_user, params = {})
+ @current_user, @params = current_user, params.dup
+ @user = params.delete(:user)
end
end
end
diff --git a/app/services/emails/confirm_service.rb b/app/services/emails/confirm_service.rb
new file mode 100644
index 00000000000..b5301bf2b82
--- /dev/null
+++ b/app/services/emails/confirm_service.rb
@@ -0,0 +1,7 @@
+module Emails
+ class ConfirmService < ::Emails::BaseService
+ def execute(email)
+ email.resend_confirmation_instructions
+ end
+ end
+end
diff --git a/app/services/emails/create_service.rb b/app/services/emails/create_service.rb
index b6491ee9804..94a841af7c3 100644
--- a/app/services/emails/create_service.rb
+++ b/app/services/emails/create_service.rb
@@ -1,7 +1,7 @@
module Emails
class CreateService < ::Emails::BaseService
- def execute
- @user.emails.create(email: @email)
+ def execute(extra_params = {})
+ @user.emails.create(@params.merge(extra_params))
end
end
end
diff --git a/app/services/emails/destroy_service.rb b/app/services/emails/destroy_service.rb
index 44011cc36c8..1ed131fe326 100644
--- a/app/services/emails/destroy_service.rb
+++ b/app/services/emails/destroy_service.rb
@@ -1,7 +1,7 @@
module Emails
class DestroyService < ::Emails::BaseService
- def execute
- update_secondary_emails! if Email.find_by_email!(@email).destroy
+ def execute(email)
+ email.destroy && update_secondary_emails!
end
private
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 12604e7eb5d..d61a342ebad 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -43,6 +43,10 @@ class IssuableBaseService < BaseService
SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
end
+ def create_discussion_lock_note(issuable)
+ SystemNoteService.discussion_lock(issuable, current_user)
+ end
+
def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}"
@@ -57,6 +61,7 @@ class IssuableBaseService < BaseService
params.delete(:due_date)
params.delete(:canonical_issue_id)
params.delete(:project)
+ params.delete(:discussion_locked)
end
filter_assignee(issuable)
@@ -236,6 +241,7 @@ class IssuableBaseService < BaseService
handle_common_system_notes(issuable, old_labels: old_labels)
end
+ change_discussion_lock(issuable)
handle_changes(
issuable,
old_labels: old_labels,
@@ -249,7 +255,7 @@ class IssuableBaseService < BaseService
invalidate_cache_counts(issuable, users: affected_assignees.compact)
after_update(issuable)
issuable.create_new_cross_references!(current_user)
- execute_hooks(issuable, 'update')
+ execute_hooks(issuable, 'update', old_labels: old_labels, old_assignees: old_assignees)
issuable.update_project_counter_caches if update_project_counters
end
@@ -294,6 +300,12 @@ class IssuableBaseService < BaseService
end
end
+ def change_discussion_lock(issuable)
+ if issuable.previous_changes.include?('discussion_locked')
+ create_discussion_lock_note(issuable)
+ end
+ end
+
def toggle_award(issuable)
award = params.delete(:emoji_award)
if award
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 4c198fc96ea..735257c4779 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -1,10 +1,10 @@
module Issues
class BaseService < ::IssuableBaseService
- def hook_data(issue, action)
- issue_data = issue.to_hook_data(current_user)
- issue_url = Gitlab::UrlBuilder.build(issue)
- issue_data[:object_attributes].merge!(url: issue_url, action: action)
- issue_data
+ def hook_data(issue, action, old_labels: [], old_assignees: [])
+ hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees)
+ hook_data[:object_attributes][:action] = action
+
+ hook_data
end
def reopen_service
@@ -22,8 +22,8 @@ module Issues
issue, issue.project, current_user, old_assignees)
end
- def execute_hooks(issue, action = 'open')
- issue_data = hook_data(issue, action)
+ def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: [])
+ issue_data = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
issue.project.execute_hooks(issue_data, hooks_scope)
issue.project.execute_services(issue_data, hooks_scope)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index b4ca3966505..e0339ddf9bb 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -34,7 +34,7 @@ module Issues
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees)
- todo_service.reassigned_issue(issue, current_user)
+ todo_service.reassigned_issue(issue, current_user, old_assignees)
end
if issue.previous_changes.include?('confidential')
diff --git a/app/services/keys/last_used_service.rb b/app/services/keys/last_used_service.rb
index 066f3246158..dbd79f7da55 100644
--- a/app/services/keys/last_used_service.rb
+++ b/app/services/keys/last_used_service.rb
@@ -16,6 +16,8 @@ module Keys
end
def update?
+ return false if ::Gitlab::Database.read_only?
+
last_used = key.last_used_at
return false if last_used && (Time.zone.now - last_used) <= TIMEOUT
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 35ccff26262..112606a82d7 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -18,19 +18,19 @@ module MergeRequests
super if changed_title
end
- def hook_data(merge_request, action, oldrev = nil)
- hook_data = merge_request.to_hook_data(current_user)
- hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request)
+ def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: [])
+ hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees)
hook_data[:object_attributes][:action] = action
- if oldrev && !Gitlab::Git.blank_ref?(oldrev)
- hook_data[:object_attributes][:oldrev] = oldrev
+ if old_rev && !Gitlab::Git.blank_ref?(old_rev)
+ hook_data[:object_attributes][:oldrev] = old_rev
end
+
hook_data
end
- def execute_hooks(merge_request, action = 'open', oldrev = nil)
+ def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: [])
if merge_request.project
- merge_data = hook_data(merge_request, action, oldrev)
+ merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees)
merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
merge_request.project.execute_services(merge_data, :merge_request_hooks)
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index bc4a13cf4bc..fc100580c4f 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -166,7 +166,7 @@ module MergeRequests
# Call merge request webhook with update branches
def execute_mr_web_hooks
merge_requests_for_source_branch.each do |merge_request|
- execute_hooks(merge_request, 'update', @oldrev)
+ execute_hooks(merge_request, 'update', old_rev: @oldrev)
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index e2a80db06a6..8d5da459882 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -31,13 +31,6 @@ class NotificationService
end
end
- # Always notify user about email added to profile
- def new_email(email)
- if email.user&.can?(:receive_notifications)
- mailer.new_email_email(email.id).deliver_later
- end
- end
-
# When create an issue we should send an email to:
#
# * issue assignee if their notification level is not Disabled
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 54eb75ab9bf..19d75ff2efa 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -22,6 +22,13 @@ module Projects
Projects::UnlinkForkService.new(project, current_user).execute
+ # The project is not necessarily a fork, so update the fork network originating
+ # from this project
+ if fork_network = project.root_of_fork_network
+ fork_network.update(root_project: nil,
+ deleted_root_project_name: project.full_name)
+ end
+
attempt_destroy_transaction(project)
system_hook_service.execute_hooks_for(project, :destroy)
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index ad67e68a86a..eb5cce5ab98 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -23,11 +23,31 @@ module Projects
refresh_forks_count
+ link_fork_network(new_project)
+
new_project
end
private
+ def fork_network
+ if @project.fork_network
+ @project.fork_network
+ elsif forked_from_project = @project.forked_from_project
+ # TODO: remove this case when all background migrations have completed
+ # this only happens when a project had a `forked_project_link` that was
+ # not migrated to the `fork_network` relation
+ forked_from_project.fork_network || forked_from_project.create_root_of_fork_network
+ else
+ @project.create_root_of_fork_network
+ end
+ end
+
+ def link_fork_network(new_project)
+ fork_network.fork_network_members.create(project: new_project,
+ forked_from_project: @project)
+ end
+
def refresh_forks_count
Projects::ForksCountService.new(@project).refresh_cache
end
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index f30b40423c8..abe414d0c05 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -16,6 +16,7 @@ module Projects
refresh_forks_count(@project.forked_from_project)
@project.forked_project_link.destroy
+ @project.fork_network_member.destroy
end
def refresh_forks_count(project)
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index a077b3584b0..955d934838b 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -458,7 +458,7 @@ module QuickActions
target_branch_param.strip
end
command :target_branch do |branch_name|
- @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
+ @updates[:target_branch] = branch_name if project.repository.branch_exists?(branch_name)
end
desc 'Move issue from one column of the board to another'
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 1f66a2668f9..a52dce6cb4b 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -591,6 +591,13 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
end
+ def discussion_lock(issuable, author)
+ action = issuable.discussion_locked? ? 'locked' : 'unlocked'
+ body = "#{action} this #{issuable.class.to_s.titleize.downcase}"
+
+ create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action))
+ end
+
private
def notes_for_mentioner(mentioner, noteable, notes)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 6ee96d6a0f8..b6125cafa83 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -43,8 +43,8 @@ class TodoService
#
# * create a pending todo for new assignee if issue is assigned
#
- def reassigned_issue(issue, current_user)
- create_assignment_todo(issue, current_user)
+ def reassigned_issue(issue, current_user, old_assignees = [])
+ create_assignment_todo(issue, current_user, old_assignees)
end
# When create a merge request we should:
@@ -254,10 +254,11 @@ class TodoService
create_mention_todos(project, target, author, note, skip_users)
end
- def create_assignment_todo(issuable, author)
+ def create_assignment_todo(issuable, author, old_assignees = [])
if issuable.assignees.any?
+ assignees = issuable.assignees - old_assignees
attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
- create_todos(issuable.assignees, attributes)
+ create_todos(assignees, attributes)
end
end
diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb
index ab532a1fdcf..5803404c3c8 100644
--- a/app/services/users/activity_service.rb
+++ b/app/services/users/activity_service.rb
@@ -14,7 +14,7 @@ module Users
private
def record_activity
- Gitlab::UserActivities.record(@author.id)
+ Gitlab::UserActivities.record(@author.id) if Gitlab::Database.read_write?
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})")
end
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index e3a77dfdf10..47cc2d4d27e 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -20,7 +20,7 @@
= visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40
- = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ = group_icon(group, class: "avatar s40 hidden-xs")
.title
= link_to [:admin, group], class: 'group-name' do
= group.full_name
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 3e02f7b1e16..2545cecc721 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -16,7 +16,7 @@
%ul.well-list
%li
.avatar-container.s60
- = image_tag group_icon(@group), class: "avatar s60"
+ = group_icon(@group, class: "avatar s60")
%li
%span.light Name:
%strong= @group.name
diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml
index 0310498ae54..7066ed12b95 100644
--- a/app/views/admin/jobs/index.html.haml
+++ b/app/views/admin/jobs/index.html.haml
@@ -3,7 +3,7 @@
%div{ class: container_class }
- .top-area
+ .top-area.scrolling-tabs-container.inner-page-scroll-tabs
- build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 3f202fbf4fe..4d8754afdd2 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -4,7 +4,7 @@
%div{ class: container_class }
- .top-area
+ .top-area.scrolling-tabs-container.inner-page-scroll-tabs
.prepend-top-default
.search-holder
= render 'shared/projects/search_form', autofocus: true, icon: true
diff --git a/app/views/devise/mailer/_confirmation_instructions_account.html.haml b/app/views/devise/mailer/_confirmation_instructions_account.html.haml
new file mode 100644
index 00000000000..65565b7b8a8
--- /dev/null
+++ b/app/views/devise/mailer/_confirmation_instructions_account.html.haml
@@ -0,0 +1,16 @@
+- confirmation_link = confirmation_url(@resource, confirmation_token: @token)
+- if @resource.unconfirmed_email.present?
+ #content
+ = email_default_heading(@resource.unconfirmed_email)
+ %p Click the link below to confirm your email address.
+ #cta
+ = link_to 'Confirm your email address', confirmation_link
+- else
+ #content
+ - if Gitlab.com?
+ = email_default_heading('Thanks for signing up to GitLab!')
+ - else
+ = email_default_heading("Welcome, #{@resource.name}!")
+ %p To get started, click the link below to confirm your account.
+ #cta
+ = link_to 'Confirm your account', confirmation_link
diff --git a/app/views/devise/mailer/_confirmation_instructions_account.text.erb b/app/views/devise/mailer/_confirmation_instructions_account.text.erb
new file mode 100644
index 00000000000..01f09aa763d
--- /dev/null
+++ b/app/views/devise/mailer/_confirmation_instructions_account.text.erb
@@ -0,0 +1,14 @@
+<% if @resource.unconfirmed_email.present? %>
+<%= @resource.unconfirmed_email %>,
+
+Use the link below to confirm your email address.
+<% else %>
+ <% if Gitlab.com? %>
+Thanks for signing up to GitLab!
+ <% else %>
+Welcome, <%= @resource.name %>!
+ <% end %>
+To get started, use the link below to confirm your account.
+<% end %>
+
+<%= confirmation_url(@resource, confirmation_token: @token) %>
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
new file mode 100644
index 00000000000..3d0a1f622a5
--- /dev/null
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
@@ -0,0 +1,8 @@
+#content
+ = email_default_heading("#{@resource.user.name}, you've added an additional email!")
+ %p Click the link below to confirm your email address (#{@resource.email})
+ #cta
+ = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
+ %p
+ If this email was added in error, you can remove it here:
+ = link_to "Emails", profile_emails_url
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
new file mode 100644
index 00000000000..a3b28cb0b84
--- /dev/null
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
@@ -0,0 +1,7 @@
+<%= @resource.user.name %>, you've added an additional email!
+
+Use the link below to confirm your email address (<%= @resource.email %>)
+
+<%= confirmation_url(@resource, confirmation_token: @token) %>
+
+If this email was added in error, you can remove it here: <%= profile_emails_url %>
diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml
index 4d1037807be..50ee7b53d8f 100644
--- a/app/views/devise/mailer/confirmation_instructions.html.haml
+++ b/app/views/devise/mailer/confirmation_instructions.html.haml
@@ -1,16 +1 @@
-- confirmation_link = confirmation_url(@resource, confirmation_token: @token)
-- if @resource.unconfirmed_email.present?
- #content
- = email_default_heading(@resource.unconfirmed_email)
- %p Click the link below to confirm your email address.
- #cta
- = link_to confirmation_link, confirmation_link
-- else
- #content
- - if Gitlab.com?
- = email_default_heading('Thanks for signing up to GitLab!')
- - else
- = email_default_heading("Welcome, #{@resource.name}!")
- %p To get started, click the link below to confirm your account.
- #cta
- = link_to confirmation_link, confirmation_link
+= render partial: "confirmation_instructions_#{@resource.is_a?(User) ? 'account' : 'secondary'}"
diff --git a/app/views/devise/mailer/confirmation_instructions.text.erb b/app/views/devise/mailer/confirmation_instructions.text.erb
index 9f76edb76a4..05fddddf415 100644
--- a/app/views/devise/mailer/confirmation_instructions.text.erb
+++ b/app/views/devise/mailer/confirmation_instructions.text.erb
@@ -1,9 +1 @@
-Welcome, <%= @resource.name %>!
-
-<% if @resource.unconfirmed_email.present? %>
-You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below:
-<% else %>
-You can confirm your account through the link below:
-<% end %>
-
-<%= confirmation_url(@resource, confirmation_token: @token) %>
+<%= render partial: "confirmation_instructions_#{@resource.is_a?(User) ? 'account' : 'secondary'}" %> \ No newline at end of file
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index e6d307e5568..4b6c4581eb3 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -1,6 +1,10 @@
-- expanded = local_assigns.fetch(:expanded, true)
-%tr.notes_holder{ class: ('hide' unless expanded) }
- %td.notes_line{ colspan: 2 }
- %td.notes_content
- .content{ class: ('hide' unless expanded) }
- = render partial: "discussions/notes", collection: discussions, as: :discussion
+- if local_assigns[:on_image]
+ = render partial: "discussions/notes", collection: discussions, as: :discussion
+- else
+ -# Text diff discussions
+ - expanded = local_assigns.fetch(:expanded, true)
+ %tr.notes_holder{ class: ('hide' unless expanded) }
+ %td.notes_line{ colspan: 2 }
+ %td.notes_content
+ .content{ class: ('hide' unless expanded) }
+ = render partial: "discussions/notes", collection: discussions, as: :discussion, locals: { disable_collapse_class: true }
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 4a41be972da..f9bfc01f213 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -1,18 +1,27 @@
- diff_file = discussion.diff_file
- blob = discussion.blob
+- discussions = { discussion.original_line_code => [discussion] }
+- diff_file_class = diff_file.text? ? 'text-file' : 'js-image-file'
-.diff-file.file-holder
+.diff-file.file-holder{ class: diff_file_class }
.js-file-title.file-title.file-title-flex-parent
.file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false
- .diff-content.code.js-syntax-highlight
- %table
- - discussions = { discussion.original_line_code => [discussion] }
- = render partial: "projects/diffs/line",
- collection: discussion.truncated_diff_lines,
- as: :line,
- locals: { diff_file: diff_file,
- discussions: discussions,
- discussion_expanded: true,
- plain: true }
+ - if diff_file.text?
+ .diff-content.code.js-syntax-highlight
+ %table
+ = render partial: "projects/diffs/line",
+ collection: discussion.truncated_diff_lines,
+ as: :line,
+ locals: { diff_file: diff_file,
+ discussions: discussions,
+ discussion_expanded: true,
+ plain: true }
+ - else
+ - partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff'
+
+ = render partial: "projects/diffs/#{partial}", locals: { diff_file: diff_file, position: discussion.position.to_json, click_to_comment: false }
+
+ .note-container
+ = render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse_class: true }
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index db5ab939948..1cc227428e9 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -1,6 +1,19 @@
-.discussion-notes
- %ul.notes{ data: { discussion_id: discussion.id } }
- = render partial: "shared/notes/note", collection: discussion.notes, as: :note
+- disable_collapse_class = local_assigns.fetch(:disable_collapse_class, false)
+- collapsed_class = 'collapsed' if discussion.resolved? && !disable_collapse_class
+- badge_counter = discussion_counter + 1 if local_assigns[:discussion_counter]
+- show_toggle = local_assigns.fetch(:show_toggle, true)
+- show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false)
+
+.discussion-notes{ class: collapsed_class }
+ -# Save the first note position data so that we have a reference and can go
+ -# to the first note position when we click on a badge diff discussion
+ %ul.notes{ id: "discussion_#{discussion.id}", data: { discussion_id: discussion.id, position: discussion.notes[0].position.to_json } }
+ - if discussion.try(:on_image?) && show_toggle
+ %button.diff-notes-collapse.js-diff-notes-toggle{ type: 'button' }
+ = sprite_icon('collapse', css_class: 'collapse-icon')
+ %button.btn-transparent.badge.js-diff-notes-toggle{ type: 'button' }
+ = badge_counter
+ = render partial: "shared/notes/note", collection: discussion.notes, as: :note, locals: { badge_counter: badge_counter, show_image_comment_badge: show_image_comment_badge }
.flash-container
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 53ebdd6d2ff..9a763887b30 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -19,8 +19,7 @@
- create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
- if event.commits_count > 1
%li.commits-stat
- - if event.commits_count > 2
- %span ... and #{event.commits_count - 2} more commits.
+ %span ... and #{pluralize(event.commits_count - 1, 'more commit')}.
- if event.md_ref?
- from = event.commit_from
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 181c7bee702..a0760c2073b 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -1,7 +1,7 @@
.group-home-panel.text-center
%div{ class: container_class }
.avatar-container.s70.group-avatar
- = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
+ = group_icon(@group, class: "avatar s70 avatar-tile")
%h1.group-title
= @group.name
%span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 15606dd30fd..16038ef2f79 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -10,7 +10,7 @@
.form-group
.col-sm-offset-2.col-sm-10
.avatar-container.s160
- = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
+ = group_icon(@group, alt: '', class: 'avatar group-avatar s160')
%p.light
- if @group.avatar?
You can change your group avatar here
diff --git a/app/views/groups/milestones/_header_title.html.haml b/app/views/groups/milestones/_header_title.html.haml
index d7fabf53587..24eb39b8e2f 100644
--- a/app/views/groups/milestones/_header_title.html.haml
+++ b/app/views/groups/milestones/_header_title.html.haml
@@ -1 +1,2 @@
-- header_title group_title(@group, "Milestones", group_milestones_path(@group))
+- breadcrumb_title @milestone.title
+- add_to_breadcrumbs "Milestones", group_milestones_path(@group)
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 0d5350f873b..f1b32274664 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -37,7 +37,7 @@
- if content_for?(:library_javascripts)
= yield :library_javascripts
- = javascript_include_tag asset_path("locale/#{I18n.locale.to_s || I18n.default_locale.to_s}/app.js")
+ = javascript_include_tag asset_path("locale/#{I18n.locale.to_s || I18n.default_locale.to_s}/app.js") unless I18n.locale == :en
= webpack_bundle_tag "webpack_runtime"
= webpack_bundle_tag "common"
= webpack_bundle_tag "main"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 7e9b76da570..5ff6ac5fc00 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,4 +1,4 @@
-%header.navbar.navbar-gitlab.navbar-gitlab-new
+%header.navbar.navbar-gitlab
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
@@ -73,7 +73,7 @@
%button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation
- = sprite_icon('more', size: 16, css_class: 'more-icon js-navbar-toggle-right')
- = sprite_icon('close', size: 16, css_class: 'close-icon js-navbar-toggle-left')
+ = sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right')
+ = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
= render 'shared/outdated_browser'
diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
index 610ff9001f7..ad0d51d28f9 100644
--- a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
+++ b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
@@ -4,7 +4,7 @@
%li.dropdown
%button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip }
= icon("ellipsis-h")
- = sprite_icon("angle-right", css_class: "breadcrumbs-list-angle")
+ = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
.dropdown-menu
%ul
- @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index|
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 8cba495f7e4..0bf318b0b66 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -6,7 +6,7 @@
.context-header
= link_to group_path(@group), title: @group.name do
.avatar-container.s40.group-avatar
- = image_tag group_icon(@group), class: "avatar s40 avatar-tile"
+ = group_icon(@group, class: "avatar s40 avatar-tile")
.sidebar-context-title
= @group.name
%ul.sidebar-top-level-items
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 8765b814405..759d6ff68ea 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -146,7 +146,7 @@
= number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters]) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container
= sprite_icon('pipeline')
@@ -189,6 +189,12 @@
%span
Charts
+ - if project_nav_tab? :clusters
+ = nav_link(controller: :clusters) do
+ = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
+ %span
+ Cluster
+
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
diff --git a/app/views/notify/new_email_email.html.haml b/app/views/notify/new_email_email.html.haml
deleted file mode 100644
index 4a0448a573c..00000000000
--- a/app/views/notify/new_email_email.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%p
- Hi #{@user.name}!
-%p
- A new email was added to your account:
-%p
- email:
- %code= @email.email
-%p
- If this email was added in error, you can remove it here:
- = link_to "Emails", profile_emails_url
diff --git a/app/views/notify/new_email_email.text.erb b/app/views/notify/new_email_email.text.erb
deleted file mode 100644
index 51cba99ad0d..00000000000
--- a/app/views/notify/new_email_email.text.erb
+++ /dev/null
@@ -1,7 +0,0 @@
-Hi <%= @user.name %>!
-
-A new email was added to your account:
-
-email.................. <%= @email.email %>
-
-If this email was added in error, you can remove it here: <%= profile_emails_url %>
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 8abbd828032..7f79168dfb3 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -97,21 +97,29 @@
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0.danger-title
- Remove account
+ = s_('Profiles|Delete account')
.col-lg-8
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p
- Deleting an account has the following effects:
+ = s_('Profiles|Deleting an account has the following effects:')
= render 'users/deletion_guidance', user: current_user
- = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
+
+ #delete-account-modal{ data: { action_url: user_registration_path,
+ confirm_with_password: ('true' if current_user.confirm_deletion_with_password?),
+ username: current_user.username } }
+ %button.btn.btn-danger.disabled
+ = s_('Profiles|Delete account')
- else
- if @user.solo_owned_groups.present?
%p
- Your account is currently an owner in these groups:
+ = s_('Profiles|Your account is currently an owner in these groups:')
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
- You must transfer ownership or delete these groups before you can delete your account.
+ = s_('Profiles|You must transfer ownership or delete these groups before you can delete your account.')
- else
%p
- You don't have access to delete this user.
+ = s_("Profiles|You don't have access to delete this user.")
.append-bottom-default
+
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag('account')
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 612ecbbb96a..df1df4f5d72 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -32,19 +32,25 @@
All email addresses will be used to identify your commits.
%ul.well-list
%li
- = @primary
+ = render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? }
%span.pull-right
%span.label.label-success Primary email
- - if @primary === current_user.public_email
+ - if @primary_email === current_user.public_email
%span.label.label-info Public email
- - if @primary === current_user.notification_email
+ - if @primary_email === current_user.notification_email
%span.label.label-info Notification email
- @emails.each do |email|
%li
- = email.email
+ = render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
%span.pull-right
- if email.email === current_user.public_email
%span.label.label-info Public email
- if email.email === current_user.notification_email
%span.label.label-info Notification email
- = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-warning prepend-left-10'
+ - unless email.confirmed?
+ - confirm_title = "#{email.confirmation_sent_at ? 'Resend' : 'Send'} confirmation email"
+ = link_to confirm_title, resend_confirmation_instructions_profile_email_path(email), method: :put, class: 'btn btn-sm btn-warning prepend-left-10'
+
+ = link_to profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-danger prepend-left-10' do
+ %span.sr-only Remove
+ = icon('trash')
diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml
index b04981f90e3..5ed517c1ef6 100644
--- a/app/views/profiles/gpg_keys/_key.html.haml
+++ b/app/views/profiles/gpg_keys/_key.html.haml
@@ -3,10 +3,17 @@
= icon 'key', class: "settings-list-icon hidden-xs"
.key-list-item-info
- key.emails_with_verified_status.map do |email, verified|
- = render partial: 'email_with_badge', locals: { email: email, verified: verified }
+ = render partial: 'shared/email_with_badge', locals: { email: email, verified: verified }
.description
%code= key.fingerprint
+ - if key.subkeys.present?
+ .subkeys
+ %span.bold Subkeys:
+ %ul.subkeys-list
+ - key.subkeys.each do |subkey|
+ %li
+ %code= subkey.fingerprint
.pull-right
%span.key-created-at
created #{time_ago_with_tooltip(key.created_at)}
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 873b3045ea9..619b632918e 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,4 +1,6 @@
- empty_repo = @project.empty_repo?
+- fork_network = @project.fork_network
+- forked_from_project = @project.forked_from_project || fork_network&.root_project
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
.limit-container-width{ class: container_class }
.avatar-container.s70.project-avatar
@@ -12,11 +14,15 @@
- if @project.description.present?
= markdown_field(@project, :description)
- - if forked_from_project = @project.forked_from_project
+ - if @project.forked?
%p
- #{ s_('ForkedFromProjectPath|Forked from') }
- = link_to project_path(forked_from_project) do
- = forked_from_project.namespace.try(:name)
+ - if forked_from_project
+ #{ s_('ForkedFromProjectPath|Forked from') }
+ = link_to project_path(forked_from_project) do
+ = forked_from_project.full_name
+ - else
+ - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
+ = deleted_message % { project_name: fork_network.deleted_root_project_name }
.project-repo-buttons
.count-buttons
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 71424593f2e..770608eddff 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,5 +1,12 @@
- referenced_users = local_assigns.fetch(:referenced_users, nil)
+- if defined?(@merge_request) && @merge_request.discussion_locked?
+ .issuable-note-warning
+ = icon('lock', class: 'icon')
+ %span
+ = _('This merge request is locked.')
+ = _('Only project members can comment.')
+
.md-area
.md-header
%ul.nav-links.clearfix
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
index 8edb9be049a..a97ddb3c377 100644
--- a/app/views/projects/artifacts/_tree_file.html.haml
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -1,10 +1,17 @@
+- blob = file.blob
- path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path)
+- external_link = blob.external_link?(@build)
-%tr.tree-item{ 'data-link' => path_to_file }
- - blob = file.blob
+%tr.tree-item.js-artifact-tree-row{ data: { link: path_to_file, external_link: "#{external_link}" } }
%td.tree-item-file-name
= tree_icon('file', blob.mode, blob.name)
- = link_to path_to_file do
- %span.str-truncated= blob.name
+ - if external_link
+ = link_to path_to_file, class: 'tree-item-file-external-link js-artifact-tree-tooltip',
+ target: '_blank', rel: 'noopener noreferrer', title: _('Opens in a new window') do
+ %span.str-truncated>= blob.name
+ = icon('external-link', class: 'js-artifact-tree-external-icon')
+ - else
+ = link_to path_to_file do
+ %span.str-truncated= blob.name
%td
= number_to_human_size(blob.size, precision: 2)
diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml
new file mode 100644
index 00000000000..371cdb1e403
--- /dev/null
+++ b/app/views/projects/clusters/_form.html.haml
@@ -0,0 +1,37 @@
+.row
+ .col-sm-8.col-sm-offset-4
+ %p
+ - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
+
+ = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_errors(@cluster)
+ .form-group
+ = field.label :gcp_cluster_name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :gcp_cluster_name, class: 'form-control'
+
+ .form-group
+ = field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
+ = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
+ = field.text_field :gcp_project_id, class: 'form-control'
+
+ .form-group
+ = field.label :gcp_cluster_zone, s_('ClusterIntegration|Zone')
+ = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
+ = field.text_field :gcp_cluster_zone, class: 'form-control', placeholder: 'us-central1-a'
+
+ .form-group
+ = field.label :gcp_cluster_size, s_('ClusterIntegration|Number of nodes')
+ = field.text_field :gcp_cluster_size, class: 'form-control', placeholder: '3'
+
+ .form-group
+ = field.label :gcp_machine_type, s_('ClusterIntegration|Machine type')
+ = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
+ = field.text_field :gcp_machine_type, class: 'form-control', placeholder: 'n1-standard-4'
+
+ .form-group
+ = field.label :project_namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = field.text_field :project_namespace, class: 'form-control', placeholder: @cluster.project_namespace_placeholder
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save'
diff --git a/app/views/projects/clusters/_header.html.haml b/app/views/projects/clusters/_header.html.haml
new file mode 100644
index 00000000000..0134d46491c
--- /dev/null
+++ b/app/views/projects/clusters/_header.html.haml
@@ -0,0 +1,14 @@
+%h4.prepend-top-0
+ = s_('ClusterIntegration|Create new cluster on Google Container Engine')
+%p
+ = s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
+%ul
+ %li
+ - link_to_container_engine = link_to(s_('ClusterIntegration|access to Google Container Engine'), 'https://console.cloud.google.com', target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Your account must have %{link_to_container_engine}').html_safe % { link_to_container_engine: link_to_container_engine }
+ %li
+ - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/container-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
+ %li
+ - link_to_container_project = link_to(s_('ClusterIntegration|Google Container Engine project'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|A %{link_to_container_project} must have been created under this account').html_safe % { link_to_container_project: link_to_container_project }
diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/projects/clusters/_sidebar.html.haml
new file mode 100644
index 00000000000..761879db32b
--- /dev/null
+++ b/app/views/projects/clusters/_sidebar.html.haml
@@ -0,0 +1,7 @@
+%h4.prepend-top-0
+ = s_('ClusterIntegration|Cluster integration')
+%p
+ = s_('ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
+%p
+ - link = link_to(s_('ClusterIntegration|cluster'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link }
diff --git a/app/views/projects/clusters/login.html.haml b/app/views/projects/clusters/login.html.haml
new file mode 100644
index 00000000000..ae132672b7e
--- /dev/null
+++ b/app/views/projects/clusters/login.html.haml
@@ -0,0 +1,16 @@
+- breadcrumb_title "Cluster"
+- page_title _("Login")
+
+.row.prepend-top-default
+ .col-sm-4
+ = render 'sidebar'
+ .col-sm-8
+ = render 'header'
+.row
+ .col-sm-8.col-sm-offset-4.signin-with-google
+ - if @authorize_url
+ = link_to @authorize_url do
+ = image_tag('auth_buttons/signin_with_google.png')
+ - else
+ - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')
+ = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
new file mode 100644
index 00000000000..c538d41ffad
--- /dev/null
+++ b/app/views/projects/clusters/new.html.haml
@@ -0,0 +1,9 @@
+- breadcrumb_title "Cluster"
+- page_title _("New Cluster")
+
+.row.prepend-top-default
+ .col-sm-4
+ = render 'sidebar'
+ .col-sm-8
+ = render 'header'
+= render 'form'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
new file mode 100644
index 00000000000..aee6f904a62
--- /dev/null
+++ b/app/views/projects/clusters/show.html.haml
@@ -0,0 +1,70 @@
+- breadcrumb_title "Cluster"
+- page_title _("Cluster")
+
+- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) && @cluster.on_creation?
+.row.prepend-top-default.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
+ toggle_status: @cluster.enabled? ? 'true': 'false',
+ cluster_status: @cluster.status_name,
+ cluster_status_reason: @cluster.status_reason } }
+ .col-sm-4
+ = render 'sidebar'
+ .col-sm-8
+ %label.append-bottom-10{ for: 'enable-cluster-integration' }
+ = s_('ClusterIntegration|Enable cluster integration')
+ %p
+ - if @cluster.enabled?
+ - if can?(current_user, :update_cluster, @cluster)
+ = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
+ - else
+ = s_('ClusterIntegration|Cluster integration is enabled for this project.')
+ - else
+ = s_('ClusterIntegration|Cluster integration is disabled for this project.')
+
+ = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_errors(@cluster)
+ .form-group.append-bottom-20
+ %label.append-bottom-10
+ = field.hidden_field :enabled, { class: 'js-toggle-input'}
+
+ %button{ type: 'button',
+ class: "js-toggle-cluster project-feature-toggle #{'checked' unless !@cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, @cluster)}",
+ 'aria-label': s_('ClusterIntegration|Toggle Cluster'),
+ disabled: !can?(current_user, :update_cluster, @cluster),
+ data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled' } }
+
+ - if can?(current_user, :update_cluster, @cluster)
+ .form-group
+ = field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success'
+
+ - if can?(current_user, :admin_cluster, @cluster)
+ %label.append-bottom-10{ for: 'google-container-engine' }
+ = s_('ClusterIntegration|Google Container Engine')
+ %p
+ - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
+
+ .hidden.js-cluster-error.alert.alert-danger.alert-block{ role: 'alert' }
+ = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
+ %p.js-error-reason
+
+ .hidden.js-cluster-creating.alert.alert-info.alert-block{ role: 'alert' }
+ = s_('ClusterIntegration|Cluster is being created on Google Container Engine...')
+
+ .hidden.js-cluster-success.alert.alert-success.alert-block{ role: 'alert' }
+ = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine')
+
+ .form_group.append-bottom-20
+ %label.append-bottom-10{ for: 'cluter-name' }
+ = s_('ClusterIntegration|Cluster name')
+ .input-group
+ %input.form-control.cluster-name{ value: @cluster.gcp_cluster_name, disabled: true }
+ %span.input-group-addon.clipboard-addon
+ = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name'))
+
+ - if can?(current_user, :admin_cluster, @cluster)
+ .well.form_group
+ %label.text-danger
+ = s_('ClusterIntegration|Remove cluster integration')
+ %p
+ = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project.')
+ = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"})
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index c06e9f323af..71d30da14a9 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -6,18 +6,9 @@
#cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
- if @cycle_analytics_no_data
- .landing.content-block{ "v-if" => "!isOverviewDialogDismissed" }
- %button.dismiss-button{ type: 'button', 'aria-label': 'Dismiss Cycle Analytics introduction box', "@click" => "dismissOverviewDialog()" }
- = icon("times")
- .svg-container
- = custom_icon('icon_cycle_analytics_splash')
- .inner-content
- %h4
- {{ __('Introducing Cycle Analytics') }}
- %p
- {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }}
- %p
- = link_to _('Read more'), help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
+ %banner{ "v-if" => "!isOverviewDialogDismissed",
+ "documentation-link": help_page_path('user/project/cycle_analytics'),
+ "v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" }
= icon("spinner spin", "v-show" => "isLoading")
.wrapper{ "v-show" => "!isLoading && !hasError" }
.panel.panel-default
diff --git a/app/views/projects/diffs/_image_diff_frame.html.haml b/app/views/projects/diffs/_image_diff_frame.html.haml
new file mode 100644
index 00000000000..dae73e10460
--- /dev/null
+++ b/app/views/projects/diffs/_image_diff_frame.html.haml
@@ -0,0 +1,5 @@
+- class_name = local_assigns.fetch(:class_name, '')
+- note_type = local_assigns.fetch(:note_type, '')
+
+.frame{ class: class_name, data: { position: position, note_type: note_type } }
+ = image_tag(image_path, alt: alt, draggable: false, lazy: false)
diff --git a/app/views/projects/diffs/_replaced_image_diff.html.haml b/app/views/projects/diffs/_replaced_image_diff.html.haml
new file mode 100644
index 00000000000..8fc232b464e
--- /dev/null
+++ b/app/views/projects/diffs/_replaced_image_diff.html.haml
@@ -0,0 +1,61 @@
+- blob = diff_file.blob
+- old_blob = diff_file.old_blob
+- blob_raw_path = diff_file_blob_raw_path(diff_file)
+- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file)
+- click_to_comment = local_assigns.fetch(:click_to_comment, true)
+- diff_view_data = local_assigns.fetch(:diff_view_data, '')
+- class_name = ''
+
+- if click_to_comment
+ - class_name = 'js-add-image-diff-note-button click-to-comment'
+
+.image.js-replaced-image{ data: diff_view_data }
+ .two-up.view
+ .wrap
+ .frame.deleted
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
+ %p.image-info.hide
+ %span.meta-filesize= number_to_human_size(old_blob.size)
+ |
+ %strong W:
+ %span.meta-width
+ |
+ %strong H:
+ %span.meta-height
+ .wrap
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path }
+ %p.image-info.hide
+ %span.meta-filesize= number_to_human_size(blob.size)
+ |
+ %strong W:
+ %span.meta-width
+ |
+ %strong H:
+ %span.meta-height
+
+ .swipe.view.hide
+ .swipe-frame
+ .frame.deleted
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
+ .swipe-wrap
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path }
+ %span.swipe-bar
+ %span.top-handle
+ %span.bottom-handle
+
+ .onion-skin.view.hide
+ .onion-skin-frame
+ .frame.deleted
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path }
+ .controls
+ .transparent
+ .drag-track
+ .dragger{ :style => "left: 0px;" }
+ .opaque
+
+.view-modes.hide
+ %ul.view-modes-menu
+ %li.two-up{ data: { mode: 'two-up' } } 2-up
+ %li.swipe{ data: { mode: 'swipe' } } Swipe
+ %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin
diff --git a/app/views/projects/diffs/_single_image_diff.html.haml b/app/views/projects/diffs/_single_image_diff.html.haml
new file mode 100644
index 00000000000..6b0c6bbe48f
--- /dev/null
+++ b/app/views/projects/diffs/_single_image_diff.html.haml
@@ -0,0 +1,16 @@
+- blob = diff_file.blob
+- old_blob = diff_file.old_blob
+- blob_raw_path = diff_file_blob_raw_path(diff_file)
+- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file)
+- click_to_comment = local_assigns.fetch(:click_to_comment, true)
+- diff_view_data = local_assigns.fetch(:diff_view_data, '')
+- class_name = ''
+
+- if click_to_comment
+ - class_name = 'js-add-image-diff-note-button click-to-comment'
+
+.image.js-single-image{ data: diff_view_data }
+ .wrap
+ - single_class_name = diff_file.deleted_file? ? 'deleted' : 'added'
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "#{single_class_name} #{class_name} js-image-frame", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.file_path }
+ %p.image-info= number_to_human_size(blob.size)
diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml
index 6b5233833c6..f190073c2fc 100644
--- a/app/views/projects/diffs/viewers/_image.html.haml
+++ b/app/views/projects/diffs/viewers/_image.html.haml
@@ -1,67 +1,13 @@
- diff_file = viewer.diff_file
-- blob = diff_file.blob
-- old_blob = diff_file.old_blob
-- blob_raw_path = diff_file_blob_raw_path(diff_file)
-- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file)
+- image_point = Gitlab::Diff::ImagePoint.new(nil, nil, nil, nil)
+- discussions = @grouped_diff_discussions[diff_file.new_path] if @grouped_diff_discussions
+
+- locals = { diff_file: diff_file, position: diff_file.position(image_point, position_type: :image).to_json, click_to_comment: true, diff_view_data: diff_view_data }
- if diff_file.new_file? || diff_file.deleted_file?
- .image
- %span.wrap
- .frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') }
- = image_tag(blob_raw_path, alt: diff_file.file_path)
- %p.image-info= number_to_human_size(blob.size)
+ = render partial: "projects/diffs/single_image_diff", locals: locals
- else
- .image
- .two-up.view
- %span.wrap
- .frame.deleted
- = image_tag(old_blob_raw_path, alt: diff_file.old_path)
- %p.image-info.hide
- %span.meta-filesize= number_to_human_size(old_blob.size)
- |
- %b W:
- %span.meta-width
- |
- %b H:
- %span.meta-height
- %span.wrap
- .frame.added
- = image_tag(blob_raw_path, alt: diff_file.new_path)
- %p.image-info.hide
- %span.meta-filesize= number_to_human_size(blob.size)
- |
- %b W:
- %span.meta-width
- |
- %b H:
- %span.meta-height
-
- .swipe.view.hide
- .swipe-frame
- .frame.deleted
- = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
- .swipe-wrap
- .frame.added
- = image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false)
- %span.swipe-bar
- %span.top-handle
- %span.bottom-handle
-
- .onion-skin.view.hide
- .onion-skin-frame
- .frame.deleted
- = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
- .frame.added
- = image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false)
- .controls
- .transparent
- .drag-track
- .dragger{ :style => "left: 0px;" }
- .opaque
-
+ = render partial: "projects/diffs/replaced_image_diff", locals: locals
- .view-modes.hide
- %ul.view-modes-menu
- %li.two-up{ data: { mode: 'two-up' } } 2-up
- %li.swipe{ data: { mode: 'swipe' } } Swipe
- %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin
+.note-container
+ = render partial: "discussions/notes", collection: discussions, as: :discussion
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 8ae4fd94146..893e536e289 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -97,7 +97,7 @@
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
- Perform advanced options such as housekeeping, exporting, archiving, renaming, transferring, or removing your project.
+ Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
.settings-content.no-animate{ class: ('expanded' if expanded) }
.sub-section
%h4 Housekeeping
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index fbaf88356bf..b9fec8af4d7 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -27,7 +27,9 @@
.issuable-meta
- if @issue.confidential
- = icon('eye-slash', class: 'is-confidential')
+ = icon('eye-slash', class: 'issuable-warning-icon')
+ - if @issue.discussion_locked?
+ = icon('lock', class: 'issuable-warning-icon')
= issuable_meta(@issue, @project, "Issue")
.issuable-actions.js-issuable-actions
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 43e23bb2200..d5c6d329ce4 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -48,7 +48,7 @@
- if @build.trigger_variables.any?
%p
- %button.btn.group.btn-group-justified.reveal-variables Reveal Variables
+ %button.btn.group.btn-group-justified.js-reveal-variables Reveal Variables
%dl.js-build-variables.trigger-build-variables.hide
- @build.trigger_variables.each do |trigger_variable|
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index 4a238b99b58..9963cc93633 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -8,7 +8,7 @@
.nav-controls
- if can?(current_user, :update_build, @project)
- - if @all_builds.running_or_pending.any?
+ - if @all_builds.running_or_pending.limit(1).any?
= link_to 'Cancel running', cancel_all_project_jobs_path(@project),
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 9ff85c2ee4c..cb723fe6a18 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -15,6 +15,8 @@
= icon('angle-double-left')
.issuable-meta
+ - if @merge_request.discussion_locked?
+ = icon('lock', class: 'issuable-warning-icon')
= issuable_meta(@merge_request, @project, "Merge request")
.issuable-actions.js-issuable-actions
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index d3742f3e4be..d88e3d794d3 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -83,7 +83,7 @@
#pipelines.pipelines.tab-pane
- if @pipelines.any?
= render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request)
- #diffs.diffs.tab-pane
+ #diffs.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked? } }
-# This tab is always loaded via AJAX
.mr-loading-status
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index fda068f08c2..7062c5b765e 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,5 +1,5 @@
- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout
-- add_to_breadcrumbs "Snippets", dashboard_snippets_path
+- add_to_breadcrumbs "Snippets", project_snippets_path(@project)
- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
diff --git a/app/views/projects/tree/_old_tree_content.html.haml b/app/views/projects/tree/_old_tree_content.html.haml
index 820b947804e..6ea78851b8d 100644
--- a/app/views/projects/tree/_old_tree_content.html.haml
+++ b/app/views/projects/tree/_old_tree_content.html.haml
@@ -6,7 +6,7 @@
%th= s_('ProjectFileTree|Name')
%th.hidden-xs
.pull-left= _('Last commit')
- %th.text-right= _('Last Update')
+ %th.text-right= _('Last update')
- if @path.present?
%tr.tree-item
%td.tree-item-file-name
diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml
index 911e1339541..d6e568bac94 100644
--- a/app/views/projects/wikis/empty.html.haml
+++ b/app/views/projects/wikis/empty.html.haml
@@ -1,6 +1,6 @@
- page_title _("Wiki")
-%h3.page-title= _("Wiki|Empty page")
+%h3.page-title= s_("Wiki|Empty page")
%hr
.error_message
= s_("WikiEmptyPageError|You are not allowed to create wiki pages")
diff --git a/app/views/profiles/gpg_keys/_email_with_badge.html.haml b/app/views/shared/_email_with_badge.html.haml
index 5f7844584e1..b7bbc109238 100644
--- a/app/views/profiles/gpg_keys/_email_with_badge.html.haml
+++ b/app/views/shared/_email_with_badge.html.haml
@@ -2,7 +2,7 @@
- css_classes << (verified ? 'verified': 'unverified')
- text = verified ? 'Verified' : 'Unverified'
-.gpg-email-badge
- .gpg-email-badge-email= email
+.email-badge
+ .email-badge-email= email
%div{ class: css_classes }
= text
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 3baa956b910..639f28cc210 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -3,22 +3,22 @@
= link_to build_path_proc.call(nil) do
All
%span.badge.js-totalbuilds-count
- = number_with_delimiter(all_builds.count(:id))
+ = limited_counter_with_delimiter(all_builds)
%li{ class: active_when(scope == 'pending') }>
= link_to build_path_proc.call('pending') do
Pending
%span.badge
- = number_with_delimiter(all_builds.pending.count(:id))
+ = limited_counter_with_delimiter(all_builds.pending)
%li{ class: active_when(scope == 'running') }>
= link_to build_path_proc.call('running') do
Running
%span.badge
- = number_with_delimiter(all_builds.running.count(:id))
+ = limited_counter_with_delimiter(all_builds.running)
%li{ class: active_when(scope == 'finished') }>
= link_to build_path_proc.call('finished') do
Finished
%span.badge
- = number_with_delimiter(all_builds.finished.count(:id))
+ = limited_counter_with_delimiter(all_builds.finished)
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index eb41daa0988..059dd24be6d 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -28,7 +28,7 @@
.avatar-container.s40
= link_to group do
- = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ = group_icon(group, class: "avatar s40 hidden-xs")
.title
= link_to group_name, group, class: 'group-name'
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 674f13ddb23..7b7411b1e23 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -119,6 +119,10 @@
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point
+ - if issuable.has_attribute?(:discussion_locked)
+ %script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
+ #js-lock-entry-point
+
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user
- subscribed = issuable.subscribed?(current_user, @project)
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index bcdad3c153a..5868c52566d 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -4,7 +4,7 @@
- dom_id = "group_member_#{group_link.id}"
%li.member.group_member{ id: dom_id }
%span.list-item-name
- = image_tag group_icon(group), class: "avatar s40", alt: ''
+ = group_icon(group, class: "avatar s40", alt: '')
%strong
= link_to group.full_name, group_path(group)
.cgray
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index 725bf916592..71c0d740bc8 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -24,20 +24,21 @@
-# DiffNote
= f.hidden_field :position
- = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
- = render 'projects/zen', f: f,
- attr: :note,
- classes: 'note-textarea js-note-text',
- placeholder: "Write a comment or drag your files here...",
- supports_quick_actions: supports_quick_actions,
- supports_autocomplete: supports_autocomplete
- = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
- .error-alert
-
- .note-form-actions.clearfix
- = render partial: 'shared/notes/comment_button'
-
- = yield(:note_actions)
-
- %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
- Discard draft
+ .discussion-form-container
+ = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
+ = render 'projects/zen', f: f,
+ attr: :note,
+ classes: 'note-textarea js-note-text',
+ placeholder: "Write a comment or drag your files here...",
+ supports_quick_actions: supports_quick_actions,
+ supports_autocomplete: supports_autocomplete
+ = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
+ .error-alert
+
+ .note-form-actions.clearfix
+ = render partial: 'shared/notes/comment_button'
+
+ = yield(:note_actions)
+
+ %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
+ Discard draft
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 4f00a9f2759..b6085fd3af0 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -1,7 +1,10 @@
- return unless note.author
- return if note.cross_reference_not_visible_for?(current_user)
+- show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false)
- note_editable = note_editable?(note)
+- note_counter = local_assigns.fetch(:note_counter, 0)
+
%li.timeline-entry{ id: dom_id(note),
class: ["note", "note-row-#{note.id}", ('system-note' if note.system)],
data: { author_id: note.author.id,
@@ -12,8 +15,18 @@
- if note.system
= icon_for_system_note(note)
- else
- %a{ href: user_path(note.author) }
+ %a.image-diff-avatar-link{ href: user_path(note.author) }
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
+ - if note.is_a?(DiffNote) && note.on_image?
+ - if show_image_comment_badge && note_counter == 0
+ -# Only show this for the first comment in the discussion
+ %span.image-comment-badge.inverted
+ = icon('comment-o')
+ - elsif note_counter == 0
+ - counter = badge_counter if local_assigns[:badge_counter]
+ - badge_class = "hidden" if @fresh_discussion || counter.nil?
+ %span.badge{ class: badge_class }
+ = counter
.timeline-content
.note-header
.note-header-info
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index e3e86709b8f..c6e18108c7a 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -1,3 +1,6 @@
+- issuable = @issue || @merge_request
+- discussion_locked = issuable&.discussion_locked?
+
%ul#notes-list.notes.main-notes-list.timeline
= render "shared/notes/notes"
@@ -21,5 +24,14 @@
or
= link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link'
to comment
-
+- elsif discussion_locked
+ .disabled-comment.text-center.prepend-top-default
+ %span.issuable-note-warning
+ %span.icon= sprite_icon('lock', size: 14)
+ %span
+ This
+ = issuable.class.to_s.titleize.downcase
+ is locked. Only
+ %b project members
+ can comment.
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml
index eff6c80d144..55799e10a46 100644
--- a/app/views/users/_groups.html.haml
+++ b/app/views/users/_groups.html.haml
@@ -2,4 +2,4 @@
- groups.each do |group|
= link_to group, class: 'profile-groups-avatars inline', title: group.name do
.avatar-container.s40
- = image_tag group_icon(group), class: 'avatar group-avatar s40'
+ = group_icon(group, class: 'avatar group-avatar s40')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index d0ffcc88d43..6c3cd6ecefe 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -9,7 +9,7 @@
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
.user-profile
- .cover-block.user-cover-block.layout-nav
+ .cover-block.user-cover-block.top-area
.cover-controls
- if @user == current_user
= link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index e2a1b3dcc41..52e7d346e74 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -6,6 +6,7 @@ class BuildFinishedWorker
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
+ BuildTraceSectionsWorker.perform_async(build.id)
BuildCoverageWorker.new.perform(build.id)
BuildHooksWorker.new.perform(build.id)
end
diff --git a/app/workers/build_trace_sections_worker.rb b/app/workers/build_trace_sections_worker.rb
new file mode 100644
index 00000000000..8c57e8f767b
--- /dev/null
+++ b/app/workers/build_trace_sections_worker.rb
@@ -0,0 +1,8 @@
+class BuildTraceSectionsWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ def perform(build_id)
+ Ci::Build.find_by(id: build_id)&.parse_trace_sections!
+ end
+end
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
new file mode 100644
index 00000000000..63300b58a25
--- /dev/null
+++ b/app/workers/cluster_provision_worker.rb
@@ -0,0 +1,10 @@
+class ClusterProvisionWorker
+ include Sidekiq::Worker
+ include ClusterQueue
+
+ def perform(cluster_id)
+ Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
+ Ci::ProvisionClusterService.new.execute(cluster)
+ end
+ end
+end
diff --git a/app/workers/concerns/cluster_queue.rb b/app/workers/concerns/cluster_queue.rb
new file mode 100644
index 00000000000..a5074d13220
--- /dev/null
+++ b/app/workers/concerns/cluster_queue.rb
@@ -0,0 +1,10 @@
+##
+# Concern for setting Sidekiq settings for the various Gcp clusters workers.
+#
+module ClusterQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: :gcp_cluster
+ end
+end
diff --git a/app/workers/concerns/project_start_import.rb b/app/workers/concerns/project_start_import.rb
new file mode 100644
index 00000000000..0704ebbb0fd
--- /dev/null
+++ b/app/workers/concerns/project_start_import.rb
@@ -0,0 +1,9 @@
+module ProjectStartImport
+ def start(project)
+ if project.import_started? && project.import_jid == self.jid
+ return true
+ end
+
+ project.import_start
+ end
+end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index cde5b45ad41..264706e3e23 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -4,6 +4,7 @@ class RepositoryForkWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
+ include ProjectStartImport
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
@@ -37,7 +38,7 @@ class RepositoryForkWorker
private
def start_fork(project)
- return true if project.import_start
+ return true if start(project)
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")
false
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 00a021abbdc..d7c0043d3b6 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -4,6 +4,7 @@ class RepositoryImportWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
include ExceptionBacktrace
+ include ProjectStartImport
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
@@ -34,7 +35,7 @@ class RepositoryImportWorker
private
def start_import(project)
- return true if project.import_start
+ return true if start(project)
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
new file mode 100644
index 00000000000..5aa3bbdaa9d
--- /dev/null
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -0,0 +1,27 @@
+class WaitForClusterCreationWorker
+ include Sidekiq::Worker
+ include ClusterQueue
+
+ INITIAL_INTERVAL = 2.minutes
+ EAGER_INTERVAL = 10.seconds
+ TIMEOUT = 20.minutes
+
+ def perform(cluster_id)
+ Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
+ Ci::FetchGcpOperationService.new.execute(cluster) do |operation|
+ case operation.status
+ when 'RUNNING'
+ if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc
+ return cluster.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
+ end
+
+ WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, cluster.id)
+ when 'DONE'
+ Ci::FinalizeClusterCreationService.new.execute(cluster)
+ else
+ return cluster.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
+ end
+ end
+ end
+ end
+end
diff --git a/changelogs/unreleased/18608-lock-issues.yml b/changelogs/unreleased/18608-lock-issues.yml
new file mode 100644
index 00000000000..7d907f744f6
--- /dev/null
+++ b/changelogs/unreleased/18608-lock-issues.yml
@@ -0,0 +1,4 @@
+title: Discussion lock for issues and merge requests
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/23888-fix-unsubscription-link-for-snippet-notification.yml b/changelogs/unreleased/23888-fix-unsubscription-link-for-snippet-notification.yml
new file mode 100644
index 00000000000..36bed037160
--- /dev/null
+++ b/changelogs/unreleased/23888-fix-unsubscription-link-for-snippet-notification.yml
@@ -0,0 +1,5 @@
+---
+title: Don't show an "Unsubscribe" link in snippet comment notifications
+merge_request: 14764
+author:
+type: fixed
diff --git a/changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml b/changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml
new file mode 100644
index 00000000000..6110e245013
--- /dev/null
+++ b/changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml
@@ -0,0 +1,5 @@
+---
+title: Added defaults for protected branches dropdowns on the repository settings
+merge_request: 14278
+author:
+type: changed
diff --git a/changelogs/unreleased/34102-online-view-of-artifacts-fe.yml b/changelogs/unreleased/34102-online-view-of-artifacts-fe.yml
new file mode 100644
index 00000000000..ce83b140eb6
--- /dev/null
+++ b/changelogs/unreleased/34102-online-view-of-artifacts-fe.yml
@@ -0,0 +1,5 @@
+---
+title: Add online view of HTML artifacts for public projects
+merge_request: 14399
+author:
+type: added
diff --git a/changelogs/unreleased/34284-add-changes-to-issuable-webhook-data.yml b/changelogs/unreleased/34284-add-changes-to-issuable-webhook-data.yml
new file mode 100644
index 00000000000..816e1f83111
--- /dev/null
+++ b/changelogs/unreleased/34284-add-changes-to-issuable-webhook-data.yml
@@ -0,0 +1,5 @@
+---
+title: Include the changes in issuable webhook payloads
+merge_request: 14308
+author:
+type: added
diff --git a/changelogs/unreleased/34841-todos.yml b/changelogs/unreleased/34841-todos.yml
new file mode 100644
index 00000000000..37180eefbfc
--- /dev/null
+++ b/changelogs/unreleased/34841-todos.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bad type checking to prevent 0 count badge to be shown
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml b/changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml
new file mode 100644
index 00000000000..a820ecee7d2
--- /dev/null
+++ b/changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml
@@ -0,0 +1,5 @@
+---
+title: Allow prometheus graphs to correctly handle NaN values
+merge_request: 14741
+author:
+type: fixed
diff --git a/changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml b/changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml
new file mode 100644
index 00000000000..ee6a7287e86
--- /dev/null
+++ b/changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for GPG subkeys in signature verification
+merge_request: 14517
+author:
+type: added
diff --git a/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml b/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml
new file mode 100644
index 00000000000..3364b1d46b3
--- /dev/null
+++ b/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect X-axis labels in Prometheus graphs
+merge_request: 14258
+author:
+type: fixed
diff --git a/changelogs/unreleased/37229-mr-widget-status-icon.yml b/changelogs/unreleased/37229-mr-widget-status-icon.yml
new file mode 100644
index 00000000000..6d84d1964ca
--- /dev/null
+++ b/changelogs/unreleased/37229-mr-widget-status-icon.yml
@@ -0,0 +1,5 @@
+---
+title: fix merge request widget status icon for failed CI
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37467-helper-method-from-users-endpoint-overrides-api-helper-method.yml b/changelogs/unreleased/37467-helper-method-from-users-endpoint-overrides-api-helper-method.yml
deleted file mode 100644
index 1984ec6e81c..00000000000
--- a/changelogs/unreleased/37467-helper-method-from-users-endpoint-overrides-api-helper-method.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: find_user Users helper method no longer overrides find_user API helper method.
-merge_request: 14418
-author:
-type: fixed
diff --git a/changelogs/unreleased/37483-activity-log-show-wrong-number-of-commits-per-push.yml b/changelogs/unreleased/37483-activity-log-show-wrong-number-of-commits-per-push.yml
new file mode 100644
index 00000000000..225ab9acc44
--- /dev/null
+++ b/changelogs/unreleased/37483-activity-log-show-wrong-number-of-commits-per-push.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the number representing the amount of commits related to a push event
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37552-replace-js-true-with-js.yml b/changelogs/unreleased/37552-replace-js-true-with-js.yml
new file mode 100644
index 00000000000..f7b614a8839
--- /dev/null
+++ b/changelogs/unreleased/37552-replace-js-true-with-js.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace `tag: true` into `:tag` in the specs'
+merge_request: 14653
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/37970-ci-sections-tracking.yml b/changelogs/unreleased/37970-ci-sections-tracking.yml
new file mode 100644
index 00000000000..a9011b22c6c
--- /dev/null
+++ b/changelogs/unreleased/37970-ci-sections-tracking.yml
@@ -0,0 +1,5 @@
+---
+title: Parse and store gitlab-runner timestamped section markers
+merge_request: 14551
+author:
+type: added
diff --git a/changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml b/changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml
new file mode 100644
index 00000000000..8b3fae2c103
--- /dev/null
+++ b/changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml
@@ -0,0 +1,6 @@
+---
+title: Move the deployment flag content to the left when deployment marker is near
+ the end
+merge_request: 14514
+author:
+type: fixed
diff --git a/changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml b/changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml
new file mode 100644
index 00000000000..591e542cd17
--- /dev/null
+++ b/changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml
@@ -0,0 +1,5 @@
+---
+title: Sync up hover and legend data across all graphs for the prometheus dashboard
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/38389-allow-merge-without-success.yml b/changelogs/unreleased/38389-allow-merge-without-success.yml
new file mode 100644
index 00000000000..6a37bcc55fc
--- /dev/null
+++ b/changelogs/unreleased/38389-allow-merge-without-success.yml
@@ -0,0 +1,6 @@
+---
+title: Allow merge in MR widget with no pipeline but using "Only allow merge requests
+ to be merged if the pipeline succeeds"
+merge_request: 14633
+author:
+type: fixed
diff --git a/changelogs/unreleased/38476-improve-merge-jid-cleanup-on-merge-process.yml b/changelogs/unreleased/38476-improve-merge-jid-cleanup-on-merge-process.yml
deleted file mode 100644
index 43dec51029b..00000000000
--- a/changelogs/unreleased/38476-improve-merge-jid-cleanup-on-merge-process.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust MRs being stuck on "process of being merged" for more than 2 hours
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/38528-build-url.yml b/changelogs/unreleased/38528-build-url.yml
deleted file mode 100644
index 357b9aacea8..00000000000
--- a/changelogs/unreleased/38528-build-url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes data parameter not being sent in ajax request for jobs log
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/38534-minigraph.yml b/changelogs/unreleased/38534-minigraph.yml
new file mode 100644
index 00000000000..eed240eac2d
--- /dev/null
+++ b/changelogs/unreleased/38534-minigraph.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes mini pipeline graph in commit view
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/38582-popover-badge.yml b/changelogs/unreleased/38582-popover-badge.yml
deleted file mode 100644
index ccec679a13f..00000000000
--- a/changelogs/unreleased/38582-popover-badge.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improves UX of autodevops popover to match gpg one
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml b/changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml
new file mode 100644
index 00000000000..18b1645d7a9
--- /dev/null
+++ b/changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fix project snippets breadcrumb link
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/38775-scrollable-tabs-on-admin.yml b/changelogs/unreleased/38775-scrollable-tabs-on-admin.yml
new file mode 100644
index 00000000000..65a66714bcb
--- /dev/null
+++ b/changelogs/unreleased/38775-scrollable-tabs-on-admin.yml
@@ -0,0 +1,5 @@
+---
+title: Make tabs on top scrollable on admin dashboard
+merge_request: 14685
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml b/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml
new file mode 100644
index 00000000000..bbfe5d49a3e
--- /dev/null
+++ b/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken Y-axis scaling in some Prometheus graphs
+merge_request: 14693
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml b/changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml
new file mode 100644
index 00000000000..5f5a61406da
--- /dev/null
+++ b/changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add 1000+ counters to job page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/adjusting-tooltips.yml b/changelogs/unreleased/adjusting-tooltips.yml
new file mode 100644
index 00000000000..726b75caecd
--- /dev/null
+++ b/changelogs/unreleased/adjusting-tooltips.yml
@@ -0,0 +1,5 @@
+---
+title: Adjust tooltips to adhere to 8px grid and make them more readable
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml b/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml
new file mode 100644
index 00000000000..19d950b48d6
--- /dev/null
+++ b/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid fetching all branches for branch existence checks
+merge_request: 14778
+author:
+type: changed
diff --git a/changelogs/unreleased/bvl-fix-close-issuable-link.yml b/changelogs/unreleased/bvl-fix-close-issuable-link.yml
deleted file mode 100644
index 140a9d35cc1..00000000000
--- a/changelogs/unreleased/bvl-fix-close-issuable-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix CSRF validation issue when closing/opening merge requests from the UI
-merge_request: 14555
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-fork-network-schema.yml b/changelogs/unreleased/bvl-fork-network-schema.yml
new file mode 100644
index 00000000000..97b2d5acada
--- /dev/null
+++ b/changelogs/unreleased/bvl-fork-network-schema.yml
@@ -0,0 +1,5 @@
+---
+title: Allow creating merge requests across a fork network
+merge_request: 14422
+author:
+type: changed
diff --git a/changelogs/unreleased/commit-side-by-side-comment.yml b/changelogs/unreleased/commit-side-by-side-comment.yml
deleted file mode 100644
index f9bea285a77..00000000000
--- a/changelogs/unreleased/commit-side-by-side-comment.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed commenting on side-by-side commit diff
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/content-title-link-hover-bg.yml b/changelogs/unreleased/content-title-link-hover-bg.yml
new file mode 100644
index 00000000000..c4c31c2ad06
--- /dev/null
+++ b/changelogs/unreleased/content-title-link-hover-bg.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed navbar title colors leaking out of the navbar
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/declarative-policy-optimisations.yml b/changelogs/unreleased/declarative-policy-optimisations.yml
new file mode 100644
index 00000000000..dc51c89d575
--- /dev/null
+++ b/changelogs/unreleased/declarative-policy-optimisations.yml
@@ -0,0 +1,5 @@
+---
+title: Speed up permission checks
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/dm-api-unauthorized.yml b/changelogs/unreleased/dm-api-unauthorized.yml
deleted file mode 100644
index 26b45bd4c40..00000000000
--- a/changelogs/unreleased/dm-api-unauthorized.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make sure API responds with 401 when invalid authentication info is provided
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml b/changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml
new file mode 100644
index 00000000000..14b35b6daee
--- /dev/null
+++ b/changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml
@@ -0,0 +1,5 @@
+---
+title: Create Kubernetes cluster on GKE from k8s service
+merge_request: 14470
+author:
+type: added
diff --git a/changelogs/unreleased/feature-verify_secondary_emails.yml b/changelogs/unreleased/feature-verify_secondary_emails.yml
new file mode 100644
index 00000000000..e1ecc527f85
--- /dev/null
+++ b/changelogs/unreleased/feature-verify_secondary_emails.yml
@@ -0,0 +1,5 @@
+---
+title: A confirmation email is now sent when adding a secondary email address
+merge_request:
+author: digitalmoksha
+type: added
diff --git a/changelogs/unreleased/fix-kubectl-180.yml b/changelogs/unreleased/fix-kubectl-180.yml
deleted file mode 100644
index beb71cecd57..00000000000
--- a/changelogs/unreleased/fix-kubectl-180.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Kubernetes integration: ensure v1.8.0 compatibility'
-merge_request: 14635
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-mr-sidebar-counter-after-merge.yml b/changelogs/unreleased/fix-mr-sidebar-counter-after-merge.yml
deleted file mode 100644
index 22a3efb8b1e..00000000000
--- a/changelogs/unreleased/fix-mr-sidebar-counter-after-merge.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix merge request counter updates after merge
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml b/changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml
new file mode 100644
index 00000000000..c57fceec92f
--- /dev/null
+++ b/changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade doorkeeper-openid_connect
+merge_request: 14372
+author: Markus Koller
+type: other
diff --git a/changelogs/unreleased/fl-fix-ca-time-component.yml b/changelogs/unreleased/fl-fix-ca-time-component.yml
new file mode 100644
index 00000000000..ecd377409ca
--- /dev/null
+++ b/changelogs/unreleased/fl-fix-ca-time-component.yml
@@ -0,0 +1,5 @@
+---
+title: Fix typo in cycle analytics breaking time component
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fork-btn-enabled-user-groups.yml b/changelogs/unreleased/fork-btn-enabled-user-groups.yml
deleted file mode 100644
index 3bd7581a961..00000000000
--- a/changelogs/unreleased/fork-btn-enabled-user-groups.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed fork button being disabled for users who can fork to a group
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/gitaly_feature_flag_metadata.yml b/changelogs/unreleased/gitaly_feature_flag_metadata.yml
new file mode 100644
index 00000000000..58e42ef9324
--- /dev/null
+++ b/changelogs/unreleased/gitaly_feature_flag_metadata.yml
@@ -0,0 +1,5 @@
+---
+title: Add client and call site metadata to Gitaly calls for better traceability
+merge_request: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14332
+author:
+type: added
diff --git a/changelogs/unreleased/group-milestones-breadcrumb.yml b/changelogs/unreleased/group-milestones-breadcrumb.yml
new file mode 100644
index 00000000000..87085759fda
--- /dev/null
+++ b/changelogs/unreleased/group-milestones-breadcrumb.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed milestone breadcrumb links
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/group-sort-dropdown-blank.yml b/changelogs/unreleased/group-sort-dropdown-blank.yml
new file mode 100644
index 00000000000..dd16892be4d
--- /dev/null
+++ b/changelogs/unreleased/group-sort-dropdown-blank.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed group sort dropdown defaulting to empty
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue_35873.yml b/changelogs/unreleased/issue_35873.yml
new file mode 100644
index 00000000000..65064b97e56
--- /dev/null
+++ b/changelogs/unreleased/issue_35873.yml
@@ -0,0 +1,5 @@
+---
+title: Commenting on image diffs
+merge_request: 14061
+author:
+type: added
diff --git a/changelogs/unreleased/jobs-sort-by-id.yml b/changelogs/unreleased/jobs-sort-by-id.yml
new file mode 100644
index 00000000000..ec2c3a17b74
--- /dev/null
+++ b/changelogs/unreleased/jobs-sort-by-id.yml
@@ -0,0 +1,5 @@
+---
+title: Sort JobsController by id, not created_at
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/mk-normalize-ldap-user-dns.yml b/changelogs/unreleased/mk-normalize-ldap-user-dns.yml
new file mode 100644
index 00000000000..5a128d6acc1
--- /dev/null
+++ b/changelogs/unreleased/mk-normalize-ldap-user-dns.yml
@@ -0,0 +1,5 @@
+---
+title: Search or compare LDAP DNs case-insensitively and ignore excess whitespace
+merge_request: 14697
+author:
+type: fixed
diff --git a/changelogs/unreleased/rc-fix-gh-import-branches-performance.yml b/changelogs/unreleased/rc-fix-gh-import-branches-performance.yml
new file mode 100644
index 00000000000..af359ce96b4
--- /dev/null
+++ b/changelogs/unreleased/rc-fix-gh-import-branches-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Improve GitHub import performance
+merge_request: 14445
+author:
+type: other
diff --git a/changelogs/unreleased/remove_repo_prefix_from_api.yml b/changelogs/unreleased/remove_repo_prefix_from_api.yml
new file mode 100644
index 00000000000..bf2075e529c
--- /dev/null
+++ b/changelogs/unreleased/remove_repo_prefix_from_api.yml
@@ -0,0 +1,5 @@
+---
+title: Remove 'Repo' prefix from API entites
+merge_request: 14694
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_merge_requests-feature.yml b/changelogs/unreleased/replace_project_merge_requests-feature.yml
new file mode 100644
index 00000000000..082c922a32b
--- /dev/null
+++ b/changelogs/unreleased/replace_project_merge_requests-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/merge_requests.feature' spinach test with an rspec analog
+merge_request: 14621
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml b/changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml
new file mode 100644
index 00000000000..c9fb042aa37
--- /dev/null
+++ b/changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml
@@ -0,0 +1,5 @@
+---
+title: Remove a SQL query from the todos index page
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/sh-fix-import-repos.yml b/changelogs/unreleased/sh-fix-import-repos.yml
deleted file mode 100644
index 5764b3bdc01..00000000000
--- a/changelogs/unreleased/sh-fix-import-repos.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix gitlab-rake gitlab:import:repos task failing
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-38646.yml b/changelogs/unreleased/sh-fix-issue-38646.yml
deleted file mode 100644
index 5c205775662..00000000000
--- a/changelogs/unreleased/sh-fix-issue-38646.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix pushes to an empty repository not invalidating has_visible_content? cache
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-username-logging.yml b/changelogs/unreleased/sh-fix-username-logging.yml
new file mode 100644
index 00000000000..dadf3fb6729
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-username-logging.yml
@@ -0,0 +1,5 @@
+---
+title: Fix username and ID not logging in production_json.log for Git activity
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-restore-all-refs-backup.yml b/changelogs/unreleased/sh-restore-all-refs-backup.yml
deleted file mode 100644
index eaac0c71dd0..00000000000
--- a/changelogs/unreleased/sh-restore-all-refs-backup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure all refs are restored on a restore from backup
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-show-all-slack-names.yml b/changelogs/unreleased/sh-show-all-slack-names.yml
new file mode 100644
index 00000000000..f970cd0fb15
--- /dev/null
+++ b/changelogs/unreleased/sh-show-all-slack-names.yml
@@ -0,0 +1,5 @@
+---
+title: Include GitLab full name in Slack messages
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/tc-geo-read-only-idea.yml b/changelogs/unreleased/tc-geo-read-only-idea.yml
new file mode 100644
index 00000000000..e1b52eef2ca
--- /dev/null
+++ b/changelogs/unreleased/tc-geo-read-only-idea.yml
@@ -0,0 +1,5 @@
+---
+title: Create idea of read-only database
+merge_request: 14688
+author:
+type: changed
diff --git a/changelogs/unreleased/valid-branch-name-dash-bug.yml b/changelogs/unreleased/valid-branch-name-dash-bug.yml
new file mode 100644
index 00000000000..89e4578b3e5
--- /dev/null
+++ b/changelogs/unreleased/valid-branch-name-dash-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent branches or tags from starting with invalid characters (e.g. -, .)
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-delete-account-modal.yml b/changelogs/unreleased/winh-delete-account-modal.yml
new file mode 100644
index 00000000000..f1e2710fdcc
--- /dev/null
+++ b/changelogs/unreleased/winh-delete-account-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Show confirmation modal before deleting account
+merge_request: 14360
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-repo-gitaly.yml b/changelogs/unreleased/zj-repo-gitaly.yml
deleted file mode 100644
index 634f6ba1b8b..00000000000
--- a/changelogs/unreleased/zj-repo-gitaly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Gitaly RepositoryExists remains opt-in for all method calls
-merge_request:
-author:
-type: fixed
diff --git a/config/application.rb b/config/application.rb
index bcb9f59ae3e..5100ec5d2b7 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -155,6 +155,9 @@ module Gitlab
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
ENV['GIT_TERMINAL_PROMPT'] = '0'
+ # Gitlab Read-only middleware support
+ config.middleware.insert_after ActionDispatch::Flash, 'Gitlab::Middleware::ReadOnly'
+
config.generators do |g|
g.factory_girl false
end
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 88771c5f5bb..1069c7be5f0 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -164,6 +164,7 @@ production: &base
host: example.com
port: 80 # Set to 443 if you serve the pages with HTTPS
https: false # Set to true if you serve the pages with HTTPS
+ artifacts_server: true
# external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages
# external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index a23b3208dab..a4b7c1a3919 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -316,15 +316,16 @@ Settings.registry['path'] = Settings.absolute(Settings.registry['path
# Pages
#
Settings['pages'] ||= Settingslogic.new({})
-Settings.pages['enabled'] = false if Settings.pages['enabled'].nil?
-Settings.pages['path'] = Settings.absolute(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"))
-Settings.pages['https'] = false if Settings.pages['https'].nil?
-Settings.pages['host'] ||= "example.com"
-Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
-Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
-Settings.pages['url'] ||= Settings.__send__(:build_pages_url)
-Settings.pages['external_http'] ||= false unless Settings.pages['external_http'].present?
-Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present?
+Settings.pages['enabled'] = false if Settings.pages['enabled'].nil?
+Settings.pages['path'] = Settings.absolute(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"))
+Settings.pages['https'] = false if Settings.pages['https'].nil?
+Settings.pages['host'] ||= "example.com"
+Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
+Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
+Settings.pages['url'] ||= Settings.__send__(:build_pages_url)
+Settings.pages['external_http'] ||= false unless Settings.pages['external_http'].present?
+Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present?
+Settings.pages['artifacts_server'] ||= Settings.pages['enabled'] if Settings.pages['artifacts_server'].nil?
#
# Git LFS
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 0ba0d791054..c6ec0aeda7b 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -175,7 +175,7 @@ Devise.setup do |config|
# Configure the default scope given to Warden. By default it's the first
# devise role declared in your routes (usually :user).
- # config.default_scope = :user
+ config.default_scope = :user # now have an :email scope as well, so set the default
# Configure sign_out behavior.
# Sign_out action can be scoped (i.e. /users/sign_out affects only :user scope).
diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb
index c58f425b19b..af174def047 100644
--- a/config/initializers/doorkeeper_openid_connect.rb
+++ b/config/initializers/doorkeeper_openid_connect.rb
@@ -1,7 +1,7 @@
Doorkeeper::OpenidConnect.configure do
issuer Gitlab.config.gitlab.url
- jws_private_key Rails.application.secrets.jws_private_key
+ signing_key Rails.application.secrets.openid_connect_signing_key
resource_owner_from_access_token do |access_token|
User.active.find_by(id: access_token.resource_owner_id)
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
index f9c1d2165d3..750a5b34f3b 100644
--- a/config/initializers/secret_token.rb
+++ b/config/initializers/secret_token.rb
@@ -25,7 +25,7 @@ def create_tokens
secret_key_base: file_secret_key || generate_new_secure_token,
otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
db_key_base: generate_new_secure_token,
- jws_private_key: generate_new_rsa_private_key
+ openid_connect_signing_key: generate_new_rsa_private_key
}
missing_secrets = set_missing_keys(defaults)
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 62d0967009a..b2da3b3dc19 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -2,7 +2,7 @@
require 'gitlab/current_settings'
-if Rails.env.production?
+def configure_sentry
# allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done
begin
sentry_enabled = Gitlab::CurrentSettings.current_application_settings.sentry_enabled
@@ -23,3 +23,5 @@ if Rails.env.production?
end
end
end
+
+configure_sentry if Rails.env.production?
diff --git a/config/routes.rb b/config/routes.rb
index 5683725c8a2..405bfcc2d8e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -87,6 +87,7 @@ Rails.application.routes.draw do
resources :issues, module: :boards, only: [:index, :update]
end
+ draw :google_api
draw :import
draw :uploads
draw :explore
diff --git a/config/routes/google_api.rb b/config/routes/google_api.rb
new file mode 100644
index 00000000000..3fb236d3d51
--- /dev/null
+++ b/config/routes/google_api.rb
@@ -0,0 +1,5 @@
+namespace :google_api do
+ resource :auth, only: [], controller: :authorizations do
+ match :callback, via: [:get, :post]
+ end
+end
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index 3e4e6111ab8..ddc852f0132 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -1,3 +1,6 @@
+# for secondary email confirmations - uses the same confirmation controller as :users
+devise_for :emails, path: 'profile/emails', controllers: { confirmations: :confirmations }
+
resource :profile, only: [:show, :update] do
member do
get :audit_log
@@ -28,7 +31,11 @@ resource :profile, only: [:show, :update] do
put :revoke
end
end
- resources :emails, only: [:index, :create, :destroy]
+ resources :emails, only: [:index, :create, :destroy] do
+ member do
+ put :resend_confirmation_instructions
+ end
+ end
resources :chat_names, only: [:index, :new, :create, :destroy] do
collection do
delete :deny
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 70d7673250c..7f0e056c884 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -183,6 +183,16 @@ constraints(ProjectUrlConstrainer.new) do
end
end
+ resources :clusters, except: [:edit] do
+ collection do
+ get :login
+ end
+
+ member do
+ get :status, format: :json
+ end
+ end
+
resources :environments, except: [:destroy] do
member do
post :stop
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 8235e3853dc..e2bb766ee47 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -62,5 +62,6 @@
- [update_user_activity, 1]
- [propagate_service_template, 1]
- [background_migration, 1]
+ - [gcp_cluster, 1]
- [project_migrate_hashed_storage, 1]
- [storage_migrator, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index c515a170d2d..8cded750a66 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -26,6 +26,7 @@ var config = {
},
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: {
+ account: './profile/account/index.js',
balsamiq_viewer: './blob/balsamiq_viewer.js',
blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js',
diff --git a/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb b/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb
index 915167b038d..8e9ab3f8acc 100644
--- a/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb
+++ b/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/Datetime
class AddArtifactsExpireDateToCiBuilds < ActiveRecord::Migration
def change
add_column :ci_builds, :artifacts_expire_at, :timestamp
diff --git a/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
index 756910a1fa0..fd7a48d881e 100644
--- a/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
+++ b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/Datetime
class AddQueuedAtToCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb b/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb
index 6ac10723c82..a5d1eca82bb 100644
--- a/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb
+++ b/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/Datetime
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb b/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb
index 7a1acdcbf69..47ba6bde856 100644
--- a/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb
+++ b/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/Datetime
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20170815221154_add_discussion_locked_to_issuable.rb b/db/migrate/20170815221154_add_discussion_locked_to_issuable.rb
new file mode 100644
index 00000000000..5bd777c53a0
--- /dev/null
+++ b/db/migrate/20170815221154_add_discussion_locked_to_issuable.rb
@@ -0,0 +1,13 @@
+class AddDiscussionLockedToIssuable < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ add_column(:merge_requests, :discussion_locked, :boolean)
+ add_column(:issues, :discussion_locked, :boolean)
+ end
+
+ def down
+ remove_column(:merge_requests, :discussion_locked)
+ remove_column(:issues, :discussion_locked)
+ end
+end
diff --git a/db/migrate/20170904092148_add_email_confirmation.rb b/db/migrate/20170904092148_add_email_confirmation.rb
new file mode 100644
index 00000000000..17ff424b319
--- /dev/null
+++ b/db/migrate/20170904092148_add_email_confirmation.rb
@@ -0,0 +1,33 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEmailConfirmation < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :emails, :confirmation_token, :string
+ add_column :emails, :confirmed_at, :datetime_with_timezone
+ add_column :emails, :confirmation_sent_at, :datetime_with_timezone
+ end
+end
diff --git a/db/migrate/20170909090114_add_email_confirmation_index.rb b/db/migrate/20170909090114_add_email_confirmation_index.rb
new file mode 100644
index 00000000000..a8c1023c482
--- /dev/null
+++ b/db/migrate/20170909090114_add_email_confirmation_index.rb
@@ -0,0 +1,36 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEmailConfirmationIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ # Not necessary to remove duplicates, as :confirmation_token is a new column
+ def up
+ add_concurrent_index :emails, :confirmation_token, unique: true
+ end
+
+ def down
+ remove_concurrent_index :emails, :confirmation_token if index_exists?(:emails, :confirmation_token)
+ end
+end
diff --git a/db/migrate/20170924094327_create_gcp_clusters.rb b/db/migrate/20170924094327_create_gcp_clusters.rb
new file mode 100644
index 00000000000..657dddcbbc4
--- /dev/null
+++ b/db/migrate/20170924094327_create_gcp_clusters.rb
@@ -0,0 +1,45 @@
+class CreateGcpClusters < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :gcp_clusters do |t|
+ # Order columns by best align scheme
+ t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.references :user, foreign_key: { on_delete: :nullify }
+ t.references :service, foreign_key: { on_delete: :nullify }
+ t.integer :status
+ t.integer :gcp_cluster_size, null: false
+
+ # Timestamps
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ # Enable/disable
+ t.boolean :enabled, default: true
+
+ # General
+ t.text :status_reason
+
+ # k8s integration specific
+ t.string :project_namespace
+
+ # Cluster details
+ t.string :endpoint
+ t.text :ca_cert
+ t.text :encrypted_kubernetes_token
+ t.string :encrypted_kubernetes_token_iv
+ t.string :username
+ t.text :encrypted_password
+ t.string :encrypted_password_iv
+
+ # GKE
+ t.string :gcp_project_id, null: false
+ t.string :gcp_cluster_zone, null: false
+ t.string :gcp_cluster_name, null: false
+ t.string :gcp_machine_type
+ t.string :gcp_operation_id
+ t.text :encrypted_gcp_token
+ t.string :encrypted_gcp_token_iv
+ end
+ end
+end
diff --git a/db/migrate/20170927161718_create_gpg_key_subkeys.rb b/db/migrate/20170927161718_create_gpg_key_subkeys.rb
new file mode 100644
index 00000000000..c03c40416a8
--- /dev/null
+++ b/db/migrate/20170927161718_create_gpg_key_subkeys.rb
@@ -0,0 +1,23 @@
+class CreateGpgKeySubkeys < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ create_table :gpg_key_subkeys do |t|
+ t.references :gpg_key, null: false, index: true, foreign_key: { on_delete: :cascade }
+
+ t.binary :keyid
+ t.binary :fingerprint
+
+ t.index :keyid, unique: true, length: Gitlab::Database.mysql? ? 20 : nil
+ t.index :fingerprint, unique: true, length: Gitlab::Database.mysql? ? 20 : nil
+ end
+
+ add_reference :gpg_signatures, :gpg_key_subkey, index: true, foreign_key: { on_delete: :nullify }
+ end
+
+ def down
+ remove_reference(:gpg_signatures, :gpg_key_subkey, index: true, foreign_key: true)
+
+ drop_table :gpg_key_subkeys
+ end
+end
diff --git a/db/migrate/20170928124105_create_fork_networks.rb b/db/migrate/20170928124105_create_fork_networks.rb
new file mode 100644
index 00000000000..ca906b953a3
--- /dev/null
+++ b/db/migrate/20170928124105_create_fork_networks.rb
@@ -0,0 +1,28 @@
+class CreateForkNetworks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :fork_networks do |t|
+ t.references :root_project,
+ references: :projects,
+ index: { unique: true }
+
+ t.string :deleted_root_project_name
+ end
+
+ add_concurrent_foreign_key :fork_networks, :projects,
+ column: :root_project_id,
+ on_delete: :nullify
+ end
+
+ def down
+ if foreign_keys_for(:fork_networks, :root_project_id).any?
+ remove_foreign_key :fork_networks, column: :root_project_id
+ end
+ drop_table :fork_networks
+ end
+end
diff --git a/db/migrate/20170928133643_create_fork_network_members.rb b/db/migrate/20170928133643_create_fork_network_members.rb
new file mode 100644
index 00000000000..836f023efdc
--- /dev/null
+++ b/db/migrate/20170928133643_create_fork_network_members.rb
@@ -0,0 +1,26 @@
+class CreateForkNetworkMembers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :fork_network_members do |t|
+ t.references :fork_network, null: false, index: true, foreign_key: { on_delete: :cascade }
+ t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.references :forked_from_project, references: :projects
+ end
+
+ add_concurrent_foreign_key :fork_network_members, :projects,
+ column: :forked_from_project_id,
+ on_delete: :nullify
+ end
+
+ def down
+ if foreign_keys_for(:fork_network_members, :forked_from_project_id).any?
+ remove_foreign_key :fork_network_members, column: :forked_from_project_id
+ end
+ drop_table :fork_network_members
+ end
+end
diff --git a/db/migrate/20170929080234_add_failure_reason_to_pipelines.rb b/db/migrate/20170929080234_add_failure_reason_to_pipelines.rb
new file mode 100644
index 00000000000..82adddbc1ec
--- /dev/null
+++ b/db/migrate/20170929080234_add_failure_reason_to_pipelines.rb
@@ -0,0 +1,9 @@
+class AddFailureReasonToPipelines < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_pipelines, :failure_reason, :integer
+ end
+end
diff --git a/db/migrate/20170929131201_populate_fork_networks.rb b/db/migrate/20170929131201_populate_fork_networks.rb
new file mode 100644
index 00000000000..1214962770f
--- /dev/null
+++ b/db/migrate/20170929131201_populate_fork_networks.rb
@@ -0,0 +1,30 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PopulateForkNetworks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ MIGRATION = 'PopulateForkNetworksRange'.freeze
+ BATCH_SIZE = 100
+ DELAY_INTERVAL = 15.seconds
+
+ disable_ddl_transaction!
+
+ class ForkedProjectLink < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'forked_project_links'
+ end
+
+ def up
+ say 'Populating the `fork_networks` based on existing `forked_project_links`'
+
+ queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # nothing
+ end
+end
diff --git a/db/migrate/20171006090001_create_ci_build_trace_sections.rb b/db/migrate/20171006090001_create_ci_build_trace_sections.rb
new file mode 100644
index 00000000000..ab5ef319618
--- /dev/null
+++ b/db/migrate/20171006090001_create_ci_build_trace_sections.rb
@@ -0,0 +1,19 @@
+class CreateCiBuildTraceSections < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :ci_build_trace_sections do |t|
+ t.references :project, null: false, index: true, foreign_key: { on_delete: :cascade }
+ t.datetime_with_timezone :date_start, null: false
+ t.datetime_with_timezone :date_end, null: false
+ t.integer :byte_start, limit: 8, null: false
+ t.integer :byte_end, limit: 8, null: false
+ t.integer :build_id, null: false
+ t.integer :section_name_id, null: false
+ end
+
+ add_index :ci_build_trace_sections, [:build_id, :section_name_id], unique: true
+ end
+end
diff --git a/db/migrate/20171006090010_add_build_foreign_key_to_ci_build_trace_sections.rb b/db/migrate/20171006090010_add_build_foreign_key_to_ci_build_trace_sections.rb
new file mode 100644
index 00000000000..d279463eb4b
--- /dev/null
+++ b/db/migrate/20171006090010_add_build_foreign_key_to_ci_build_trace_sections.rb
@@ -0,0 +1,15 @@
+class AddBuildForeignKeyToCiBuildTraceSections < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(:ci_build_trace_sections, :ci_builds, column: :build_id)
+ end
+
+ def down
+ remove_foreign_key(:ci_build_trace_sections, column: :build_id)
+ end
+end
diff --git a/db/migrate/20171006090100_create_ci_build_trace_section_names.rb b/db/migrate/20171006090100_create_ci_build_trace_section_names.rb
new file mode 100644
index 00000000000..88f3e60699a
--- /dev/null
+++ b/db/migrate/20171006090100_create_ci_build_trace_section_names.rb
@@ -0,0 +1,19 @@
+class CreateCiBuildTraceSectionNames < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ create_table :ci_build_trace_section_names do |t|
+ t.references :project, null: false, foreign_key: { on_delete: :cascade }
+ t.string :name, null: false
+ end
+
+ add_index :ci_build_trace_section_names, [:project_id, :name], unique: true
+ end
+
+ def down
+ remove_foreign_key :ci_build_trace_section_names, column: :project_id
+ drop_table :ci_build_trace_section_names
+ end
+end
diff --git a/db/migrate/20171006091000_add_name_foreign_key_to_ci_build_trace_sections.rb b/db/migrate/20171006091000_add_name_foreign_key_to_ci_build_trace_sections.rb
new file mode 100644
index 00000000000..08422885a98
--- /dev/null
+++ b/db/migrate/20171006091000_add_name_foreign_key_to_ci_build_trace_sections.rb
@@ -0,0 +1,15 @@
+class AddNameForeignKeyToCiBuildTraceSections < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(:ci_build_trace_sections, :ci_build_trace_section_names, column: :section_name_id)
+ end
+
+ def down
+ remove_foreign_key(:ci_build_trace_sections, column: :section_name_id)
+ end
+end
diff --git a/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb
new file mode 100644
index 00000000000..2230bb0e53c
--- /dev/null
+++ b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class NormalizeLdapExternUids < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'NormalizeLdapExternUidsRange'.freeze
+ DELAY_INTERVAL = 10.seconds
+
+ disable_ddl_transaction!
+
+ class Identity < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'identities'
+ end
+
+ def up
+ ldap_identities = Identity.where("provider like 'ldap%'")
+
+ if ldap_identities.any?
+ queue_background_migration_jobs_by_range_at_intervals(Identity, MIGRATION, DELAY_INTERVAL)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys.rb b/db/post_migrate/20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys.rb
new file mode 100644
index 00000000000..01d56fbd490
--- /dev/null
+++ b/db/post_migrate/20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys.rb
@@ -0,0 +1,28 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ScheduleCreateGpgKeySubkeysFromGpgKeys < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ MIGRATION = 'CreateGpgKeySubkeysFromGpgKeys'
+
+ class GpgKey < ActiveRecord::Base
+ self.table_name = 'gpg_keys'
+
+ include EachBatch
+ end
+
+ def up
+ GpgKey.select(:id).each_batch do |gpg_keys|
+ jobs = gpg_keys.pluck(:id).map do |id|
+ [MIGRATION, [id]]
+ end
+
+ BackgroundMigrationWorker.perform_bulk(jobs)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index fa1aad257db..aac37b6b455 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20171004121444) do
+ActiveRecord::Schema.define(version: 20171006091000) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -207,6 +207,26 @@ ActiveRecord::Schema.define(version: 20171004121444) do
add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
+ create_table "ci_build_trace_section_names", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.string "name", null: false
+ end
+
+ add_index "ci_build_trace_section_names", ["project_id", "name"], name: "index_ci_build_trace_section_names_on_project_id_and_name", unique: true, using: :btree
+
+ create_table "ci_build_trace_sections", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.datetime_with_timezone "date_start", null: false
+ t.datetime_with_timezone "date_end", null: false
+ t.integer "byte_start", limit: 8, null: false
+ t.integer "byte_end", limit: 8, null: false
+ t.integer "build_id", null: false
+ t.integer "section_name_id", null: false
+ end
+
+ add_index "ci_build_trace_sections", ["build_id", "section_name_id"], name: "index_ci_build_trace_sections_on_build_id_and_section_name_id", unique: true, using: :btree
+ add_index "ci_build_trace_sections", ["project_id"], name: "index_ci_build_trace_sections_on_project_id", using: :btree
+
create_table "ci_builds", force: :cascade do |t|
t.string "status"
t.datetime "finished_at"
@@ -342,6 +362,7 @@ ActiveRecord::Schema.define(version: 20171004121444) do
t.integer "source"
t.integer "config_source"
t.boolean "protected"
+ t.integer "failure_reason"
end
add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
@@ -514,8 +535,12 @@ ActiveRecord::Schema.define(version: 20171004121444) do
t.string "email", null: false
t.datetime "created_at"
t.datetime "updated_at"
+ t.string "confirmation_token"
+ t.datetime "confirmed_at"
+ t.datetime "confirmation_sent_at"
end
+ add_index "emails", ["confirmation_token"], name: "index_emails_on_confirmation_token", unique: true, using: :btree
add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
@@ -566,6 +591,22 @@ ActiveRecord::Schema.define(version: 20171004121444) do
add_index "features", ["key"], name: "index_features_on_key", unique: true, using: :btree
+ create_table "fork_network_members", force: :cascade do |t|
+ t.integer "fork_network_id", null: false
+ t.integer "project_id", null: false
+ t.integer "forked_from_project_id"
+ end
+
+ add_index "fork_network_members", ["fork_network_id"], name: "index_fork_network_members_on_fork_network_id", using: :btree
+ add_index "fork_network_members", ["project_id"], name: "index_fork_network_members_on_project_id", unique: true, using: :btree
+
+ create_table "fork_networks", force: :cascade do |t|
+ t.integer "root_project_id"
+ t.string "deleted_root_project_name"
+ end
+
+ add_index "fork_networks", ["root_project_id"], name: "index_fork_networks_on_root_project_id", unique: true, using: :btree
+
create_table "forked_project_links", force: :cascade do |t|
t.integer "forked_to_project_id", null: false
t.integer "forked_from_project_id", null: false
@@ -575,6 +616,45 @@ ActiveRecord::Schema.define(version: 20171004121444) do
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
+ create_table "gcp_clusters", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "user_id"
+ t.integer "service_id"
+ t.integer "status"
+ t.integer "gcp_cluster_size", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.boolean "enabled", default: true
+ t.text "status_reason"
+ t.string "project_namespace"
+ t.string "endpoint"
+ t.text "ca_cert"
+ t.text "encrypted_kubernetes_token"
+ t.string "encrypted_kubernetes_token_iv"
+ t.string "username"
+ t.text "encrypted_password"
+ t.string "encrypted_password_iv"
+ t.string "gcp_project_id", null: false
+ t.string "gcp_cluster_zone", null: false
+ t.string "gcp_cluster_name", null: false
+ t.string "gcp_machine_type"
+ t.string "gcp_operation_id"
+ t.text "encrypted_gcp_token"
+ t.string "encrypted_gcp_token_iv"
+ end
+
+ add_index "gcp_clusters", ["project_id"], name: "index_gcp_clusters_on_project_id", unique: true, using: :btree
+
+ create_table "gpg_key_subkeys", force: :cascade do |t|
+ t.integer "gpg_key_id", null: false
+ t.binary "keyid"
+ t.binary "fingerprint"
+ end
+
+ add_index "gpg_key_subkeys", ["fingerprint"], name: "index_gpg_key_subkeys_on_fingerprint", unique: true, using: :btree
+ add_index "gpg_key_subkeys", ["gpg_key_id"], name: "index_gpg_key_subkeys_on_gpg_key_id", using: :btree
+ add_index "gpg_key_subkeys", ["keyid"], name: "index_gpg_key_subkeys_on_keyid", unique: true, using: :btree
+
create_table "gpg_keys", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -598,11 +678,13 @@ ActiveRecord::Schema.define(version: 20171004121444) do
t.text "gpg_key_user_name"
t.text "gpg_key_user_email"
t.integer "verification_status", limit: 2, default: 0, null: false
+ t.integer "gpg_key_subkey_id"
end
add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree
add_index "gpg_signatures", ["gpg_key_id"], name: "index_gpg_signatures_on_gpg_key_id", using: :btree
add_index "gpg_signatures", ["gpg_key_primary_keyid"], name: "index_gpg_signatures_on_gpg_key_primary_keyid", using: :btree
+ add_index "gpg_signatures", ["gpg_key_subkey_id"], name: "index_gpg_signatures_on_gpg_key_subkey_id", using: :btree
add_index "gpg_signatures", ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree
create_table "identities", force: :cascade do |t|
@@ -660,6 +742,7 @@ ActiveRecord::Schema.define(version: 20171004121444) do
t.integer "cached_markdown_version"
t.datetime "last_edited_at"
t.integer "last_edited_by_id"
+ t.boolean "discussion_locked"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -883,6 +966,7 @@ ActiveRecord::Schema.define(version: 20171004121444) do
t.integer "head_pipeline_id"
t.boolean "ref_fetched"
t.string "merge_jid"
+ t.boolean "discussion_locked"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -1697,6 +1781,10 @@ ActiveRecord::Schema.define(version: 20171004121444) do
add_foreign_key "boards", "projects", name: "fk_f15266b5f9", on_delete: :cascade
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
+ add_foreign_key "ci_build_trace_section_names", "projects", on_delete: :cascade
+ add_foreign_key "ci_build_trace_sections", "ci_build_trace_section_names", column: "section_name_id", name: "fk_264e112c66", on_delete: :cascade
+ add_foreign_key "ci_build_trace_sections", "ci_builds", column: "build_id", name: "fk_4ebe41f502", on_delete: :cascade
+ add_foreign_key "ci_build_trace_sections", "projects", on_delete: :cascade
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
@@ -1721,8 +1809,17 @@ ActiveRecord::Schema.define(version: 20171004121444) do
add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade
add_foreign_key "events", "projects", on_delete: :cascade
add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
+ add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade
+ add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify
+ add_foreign_key "fork_network_members", "projects", on_delete: :cascade
+ add_foreign_key "fork_networks", "projects", column: "root_project_id", name: "fk_e7b436b2b5", on_delete: :nullify
add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
+ add_foreign_key "gcp_clusters", "projects", on_delete: :cascade
+ add_foreign_key "gcp_clusters", "services", on_delete: :nullify
+ add_foreign_key "gcp_clusters", "users", on_delete: :nullify
+ add_foreign_key "gpg_key_subkeys", "gpg_keys", on_delete: :cascade
add_foreign_key "gpg_keys", "users", on_delete: :cascade
+ add_foreign_key "gpg_signatures", "gpg_key_subkeys", on_delete: :nullify
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index ad904908472..ad903aef896 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -287,11 +287,11 @@ LDAP email address, and then sign into GitLab via their LDAP credentials.
There are two encryption methods, `simple_tls` and `start_tls`.
-For either encryption method, if setting `validate_certificates: false`, TLS
+For either encryption method, if setting `verify_certificates: false`, TLS
encryption is established with the LDAP server before any LDAP-protocol data is
exchanged but no validation of the LDAP server's SSL certificate is performed.
->**Note**: Before GitLab 9.5, `validate_certificates: false` is the default if
+>**Note**: Before GitLab 9.5, `verify_certificates: false` is the default if
unspecified.
## Limitations
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 3587696225c..86b436d89dd 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -142,9 +142,9 @@ and [projects APIs](../api/projects.md).
## Implementation details
When GitLab receives an artifacts archive, an archive metadata file is also
-generated. This metadata file describes all the entries that are located in the
-artifacts archive itself. The metadata file is in a binary format, with
-additional GZIP compression.
+generated by [GitLab Workhorse]. This metadata file describes all the entries
+that are located in the artifacts archive itself.
+The metadata file is in a binary format, with additional GZIP compression.
GitLab does not extract the artifacts archive in order to save space, memory
and disk I/O. It instead inspects the metadata file which contains all the
diff --git a/doc/api/issues.md b/doc/api/issues.md
index cd2cfe8e430..ec8ff3cd3f3 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -110,7 +110,8 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
- "confidential": false
+ "confidential": false,
+ "discussion_locked": false
}
]
```
@@ -216,7 +217,8 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
- "confidential": false
+ "confidential": false,
+ "discussion_locked": false
}
]
```
@@ -323,7 +325,8 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
- "confidential": false
+ "confidential": false,
+ "discussion_locked": false
}
]
```
@@ -407,6 +410,7 @@ Example response:
"human_total_time_spent": null
},
"confidential": false,
+ "discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
@@ -482,6 +486,7 @@ Example response:
"human_total_time_spent": null
},
"confidential": false,
+ "discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
@@ -515,6 +520,8 @@ PUT /projects/:id/issues/:issue_iid
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `discussion_locked` | boolean | no | Flag indicating if the issue's discussion is locked. If the discussion is locked only project members can add or edit comments. |
+
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close
@@ -558,6 +565,7 @@ Example response:
"human_total_time_spent": null
},
"confidential": false,
+ "discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
@@ -657,6 +665,7 @@ Example response:
"human_total_time_spent": null
},
"confidential": false,
+ "discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
@@ -735,6 +744,7 @@ Example response:
"human_total_time_spent": null
},
"confidential": false,
+ "discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
@@ -765,6 +775,44 @@ POST /projects/:id/issues/:issue_iid/unsubscribe
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/unsubscribe
```
+Example response:
+
+```json
+{
+ "id": 93,
+ "iid": 12,
+ "project_id": 5,
+ "title": "Incidunt et rerum ea expedita iure quibusdam.",
+ "description": "Et cumque architecto sed aut ipsam.",
+ "state": "opened",
+ "created_at": "2016-04-05T21:41:45.217Z",
+ "updated_at": "2016-04-07T13:02:37.905Z",
+ "labels": [],
+ "milestone": null,
+ "assignee": {
+ "name": "Edwardo Grady",
+ "username": "keyon",
+ "id": 21,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/keyon"
+ },
+ "author": {
+ "name": "Vivian Hermann",
+ "username": "orville",
+ "id": 11,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/orville"
+ },
+ "subscribed": false,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/12",
+ "confidential": false,
+ "discussion_locked": false
+}
+```
+
## Create a todo
Manually creates a todo for the current user on an issue. If
@@ -857,7 +905,8 @@ Example response:
"downvotes": 0,
"due_date": null,
"web_url": "http://example.com/example/example/issues/110",
- "confidential": false
+ "confidential": false,
+ "discussion_locked": false
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10",
"body": "Vel voluptas atque dicta mollitia adipisci qui at.",
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 63a5dd1445c..50a971102fb 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -201,6 +201,7 @@ Parameters:
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -276,6 +277,7 @@ Parameters:
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -387,6 +389,7 @@ Parameters:
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -480,6 +483,7 @@ POST /projects/:id/merge_requests
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -509,6 +513,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `labels` | string | no | Labels for MR as a comma-separated list |
| `milestone_id` | integer | no | The ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
+| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. |
Must include at least one non-required attribute from above.
@@ -563,6 +568,7 @@ Must include at least one non-required attribute from above.
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -667,6 +673,7 @@ Parameters:
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -743,6 +750,7 @@ Parameters:
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
@@ -1037,7 +1045,8 @@ Example response:
"id": 14,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/francisca"
+ "web_url": "https://gitlab.example.com/francisca",
+ "discussion_locked": false
},
"assignee": {
"name": "Dr. Gabrielle Strosin",
diff --git a/doc/development/README.md b/doc/development/README.md
index 1448a4c0414..b648c7ce086 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -64,9 +64,11 @@
- [Hash Indexes](hash_indexes.md)
- [Swapping Tables](swapping_tables.md)
-## i18n
+## Internationalization (i18n)
-- [Internationalization for GitLab](i18n_guide.md)
+- [Introduction](i18n/index.md)
+- [Externalization](i18n/externalization.md)
+- [Translation](i18n/translation.md)
## Compliance
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 77ae6d2a0ea..10f4c5a0902 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -88,16 +88,31 @@ followed by any global declarations, then a blank newline prior to any imports o
1. Use ES module syntax to import modules
```javascript
// bad
- require('foo');
+ const SomeClass = require('some_class');
// good
- import Foo from 'foo';
+ import SomeClass from 'some_class';
// bad
- module.exports = Foo;
+ module.exports = SomeClass;
// good
- export default Foo;
+ export default SomeClass;
+ ```
+
+ Import statements are following usual naming guidelines, for example object literals use camel case:
+
+ ```javascript
+ // some_object file
+ export default {
+ key: 'value',
+ };
+
+ // bad
+ import ObjectLiteral from 'some_object';
+
+ // good
+ import objectLiteral from 'some_object';
```
1. Relative paths: when importing a module in the same directory, a child
@@ -285,6 +300,13 @@ A forEach will cause side effects, it will be mutating the array being iterated.
1. **Extensions**: Use `.vue` extension for Vue components.
1. **Reference Naming**: Use camelCase for their instances:
```javascript
+ // bad
+ import CardBoard from 'cardBoard'
+
+ components: {
+ CardBoard:
+ };
+
// good
import cardBoard from 'cardBoard'
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
new file mode 100644
index 00000000000..167260b6e0e
--- /dev/null
+++ b/doc/development/i18n/externalization.md
@@ -0,0 +1,296 @@
+# Internationalization for GitLab
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10669) in GitLab 9.2.
+
+For working with internationalization (i18n),
+[GNU gettext](https://www.gnu.org/software/gettext/) is used given it's the most
+used tool for this task and there are a lot of applications that will help us to
+work with it.
+
+## Setting up GitLab Development Kit (GDK)
+
+In order to be able to work on the [GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-ce)
+project you must download and configure it through [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/set-up-gdk.md).
+
+Once you have the GitLab project ready, you can start working on the translation.
+
+## Tools
+
+The following tools are used:
+
+1. [`gettext_i18n_rails`](https://github.com/grosser/gettext_i18n_rails): this
+ gem allow us to translate content from models, views and controllers. Also
+ it gives us access to the following raketasks:
+ - `rake gettext:find`: Parses almost all the files from the
+ Rails application looking for content that has been marked for
+ translation. Finally, it updates the PO files with the new content that
+ it has found.
+ - `rake gettext:pack`: Processes the PO files and generates the
+ MO files that are binary and are finally used by the application.
+
+1. [`gettext_i18n_rails_js`](https://github.com/webhippie/gettext_i18n_rails_js):
+ this gem is useful to make the translations available in JavaScript. It
+ provides the following raketask:
+ - `rake gettext:po_to_json`: Reads the contents from the PO files and
+ generates JSON files containing all the available translations.
+
+1. PO editor: there are multiple applications that can help us to work with PO
+ files, a good option is [Poedit](https://poedit.net/download) which is
+ available for macOS, GNU/Linux and Windows.
+
+## Preparing a page for translation
+
+We basically have 4 types of files:
+
+1. Ruby files: basically Models and Controllers.
+1. HAML files: these are the view files.
+1. ERB files: used for email templates.
+1. JavaScript files: we mostly need to work with VUE JS templates.
+
+### Ruby files
+
+If there is a method or variable that works with a raw string, for instance:
+
+```ruby
+def hello
+ "Hello world!"
+end
+```
+
+Or:
+
+```ruby
+hello = "Hello world!"
+```
+
+You can easily mark that content for translation with:
+
+```ruby
+def hello
+ _("Hello world!")
+end
+```
+
+Or:
+
+```ruby
+hello = _("Hello world!")
+```
+
+### HAML files
+
+Given the following content in HAML:
+
+```haml
+%h1 Hello world!
+```
+
+You can mark that content for translation with:
+
+```haml
+%h1= _("Hello world!")
+```
+
+### ERB files
+
+Given the following content in ERB:
+
+```erb
+<h1>Hello world!</h1>
+```
+
+You can mark that content for translation with:
+
+```erb
+<h1><%= _("Hello world!") %></h1>
+```
+
+### JavaScript files
+
+In JavaScript we added the `__()` (double underscore parenthesis) function
+for translations.
+
+### Updating the PO files with the new content
+
+Now that the new content is marked for translation, we need to update the PO
+files with the following command:
+
+```sh
+bundle exec rake gettext:find
+```
+
+This command will update the `locale/**/gitlab.edit.po` file with the
+new content that the parser has found.
+
+New translations will be added with their default content and will be marked
+fuzzy. To use the translation, look for the `#, fuzzy` mention in `gitlab.edit.po`
+and remove it.
+
+We need to make sure we remove the `fuzzy` translations before generating the
+`locale/**/gitlab.po` file. When they aren't removed, the resulting `.po` will
+be treated as a binary file which could overwrite translations that were merged
+before the new translations.
+
+When we are just preparing a page to be translated, but not actually adding any
+translations. There's no need to generate `.po` files.
+
+Translations that aren't used in the source code anymore will be marked with
+`~#`; these can be removed to keep our translation files clutter-free.
+
+### Validating PO files
+
+To make sure we keep our translation files up to date, there's a linter that is
+running on CI as part of the `static-analysis` job.
+
+To lint the adjustments in PO files locally you can run `rake gettext:lint`.
+
+The linter will take the following into account:
+
+- Valid PO-file syntax
+- Variable usage
+ - Only one unnamed (`%d`) variable, since the order of variables might change
+ in different languages
+ - All variables used in the message-id are used in the translation
+ - There should be no variables used in a translation that aren't in the
+ message-id
+- Errors during translation.
+
+The errors are grouped per file, and per message ID:
+
+```
+Errors in `locale/zh_HK/gitlab.po`:
+ PO-syntax errors
+ SimplePoParser::ParserErrorSyntax error in lines
+ Syntax error in msgctxt
+ Syntax error in msgid
+ Syntax error in msgstr
+ Syntax error in message_line
+ There should be only whitespace until the end of line after the double quote character of a message text.
+ Parseing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
+ SimplePoParser filtered backtrace: SimplePoParser::ParserError
+Errors in `locale/zh_TW/gitlab.po`:
+ 1 pipeline
+ <%d 條流水線> is using unknown variables: [%d]
+ Failure translating to zh_TW with []: too few arguments
+```
+
+In this output the `locale/zh_HK/gitlab.po` has syntax errors.
+The `locale/zh_TW/gitlab.po` has variables that are used in the translation that
+aren't in the message with id `1 pipeline`.
+
+## Working with special content
+
+### Interpolation
+
+- In Ruby/HAML:
+
+ ```ruby
+ _("Hello %{name}") % { name: 'Joe' }
+ ```
+
+- In JavaScript: Not supported at this moment.
+
+### Plurals
+
+- In Ruby/HAML:
+
+ ```ruby
+ n_('Apple', 'Apples', 3) => 'Apples'
+ ```
+
+ Using interpolation:
+ ```ruby
+ n_("There is a mouse.", "There are %d mice.", size) % size
+ ```
+
+- In JavaScript:
+
+ ```js
+ n__('Apple', 'Apples', 3) => 'Apples'
+ ```
+
+ Using interpolation:
+
+ ```js
+ n__('Last day', 'Last %d days', 30) => 'Last 30 days'
+ ```
+
+### Namespaces
+
+Sometimes you need to add some context to the text that you want to translate
+(if the word occurs in a sentence and/or the word is ambiguous).
+
+- In Ruby/HAML:
+
+ ```ruby
+ s_('OpenedNDaysAgo|Opened')
+ ```
+
+ In case the translation is not found it will return `Opened`.
+
+- In JavaScript:
+
+ ```js
+ s__('OpenedNDaysAgo|Opened')
+ ```
+
+### Just marking content for parsing
+
+Sometimes there are some dynamic translations that can't be found by the
+parser when running `bundle exec rake gettext:find`. For these scenarios you can
+use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
+
+There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
+
+## Adding a new language
+
+Let's suppose you want to add translations for a new language, let's say French.
+
+1. The first step is to register the new language in `lib/gitlab/i18n.rb`:
+
+ ```ruby
+ ...
+ AVAILABLE_LANGUAGES = {
+ ...,
+ 'fr' => 'Français'
+ }.freeze
+ ...
+ ```
+
+1. Next, you need to add the language:
+
+ ```sh
+ bundle exec rake gettext:add_language[fr]
+ ```
+
+ If you want to add a new language for a specific region, the command is similar,
+ you just need to separate the region with an underscore (`_`). For example:
+
+ ```sh
+ bundle exec rake gettext:add_language[en_GB]
+ ```
+
+ Please note that you need to specify the region part in capitals.
+
+1. Now that the language is added, a new directory has been created under the
+ path: `locale/fr/`. You can now start using your PO editor to edit the PO file
+ located in: `locale/fr/gitlab.edit.po`.
+
+1. After you're done updating the translations, you need to process the PO files
+ in order to generate the binary MO files and finally update the JSON files
+ containing the translations:
+
+ ```sh
+ bundle exec rake gettext:compile
+ ```
+
+1. In order to see the translated content we need to change our preferred language
+ which can be found under the user's **Settings** (`/profile`).
+
+1. After checking that the changes are ok, you can proceed to commit the new files.
+ For example:
+
+ ```sh
+ git add locale/fr/ app/assets/javascripts/locale/fr/
+ git commit -m "Add French translations for Cycle Analytics page"
+ ```
diff --git a/doc/development/i18n/img/crowdin-editor.png b/doc/development/i18n/img/crowdin-editor.png
new file mode 100644
index 00000000000..5c31d8f0cec
--- /dev/null
+++ b/doc/development/i18n/img/crowdin-editor.png
Binary files differ
diff --git a/doc/development/i18n/index.md b/doc/development/i18n/index.md
new file mode 100644
index 00000000000..4cb2624c098
--- /dev/null
+++ b/doc/development/i18n/index.md
@@ -0,0 +1,76 @@
+# Translate GitLab to your language
+
+The text in GitLab's user interface is in American English by default.
+Each string can be translated to other languages.
+As each string is translated, it is added to the languages translation file,
+and will be available in future releases of GitLab.
+
+Contributions to translations are always needed.
+Many strings are not yet available for translation because they have not been externalized.
+Helping externalize strings benefits all languages.
+Some translations are incomplete or inconsistent.
+Translating strings will help complete and improve each language.
+
+## How to contribute
+
+There are many ways you can contribute in translating GitLab.
+
+### Externalize strings
+
+Before a string can be translated, it must be externalized.
+This is the process where English strings in the GitLab source code are wrapped in a function that
+retrieves the translated string for the user's language.
+
+As new features are added and existing features are updated, the surrounding strings are being
+externalized, however, there are many parts of GitLab that still need more work to externalize all
+strings.
+
+See [Externalization for GitLab](externalization.md).
+
+### Translate strings
+
+The translation process is managed at [translate.gitlab.com](https://translate.gitlab.com)
+using [Crowdin](https://crowdin.com/).
+You will need to create an account before you can submit translations.
+Once you are signed in, select the language you wish to contribute translations to.
+
+Voting for translations is also valuable, helping to confirm good and flag inaccurate translations.
+
+See [Translation guidelines](translation.md).
+
+### Proof reading
+
+Proof reading helps ensure the accuracy and consistency of translations.
+All translations are proof read before being accepted.
+If a translations requires changes, you will be notified with a comment explaining why.
+
+Community assistance proof reading translations is encouraged and appreciated.
+Requests to become a proof reader will be considered on the merits of previous translations.
+
+- Bulgarian
+- Chinese Simplified
+ - [Huang Tao](https://crowdin.com/profile/htve)
+- Chinese Traditional
+ - [Huang Tao](https://crowdin.com/profile/htve)
+- Chinese Traditional, Hong Kong
+ - [Huang Tao](https://crowdin.com/profile/htve)
+- Dutch
+- Esperanto
+- French
+- German
+- Italian
+- Japanese
+- Korean
+ - [Huang Tao](https://crowdin.com/profile/htve)
+- Portuguese, Brazilian
+- Russian
+ - [Alexy Lustin](https://crowdin.com/profile/lustin)
+ - [Nikita Grylov](https://crowdin.com/profile/nixel2007)
+- Spanish
+- Ukrainian
+
+If you would like to be added as a proof reader, please [open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues).
+
+## Release
+
+Translations are typically included in the next major or minor release.
diff --git a/doc/development/i18n/translation.md b/doc/development/i18n/translation.md
new file mode 100644
index 00000000000..79707aaf671
--- /dev/null
+++ b/doc/development/i18n/translation.md
@@ -0,0 +1,64 @@
+# Translating GitLab
+
+For managing the translation process we use [Crowdin](https://crowdin.com).
+
+## Using Crowdin
+
+The first step is to get familiar with Crowdin.
+
+### Sign In
+
+To contribute translations at [translate.gitlab.com](https://translate.gitlab.com)
+you must create a Crowdin account.
+You may create a new account or use any of their supported sign in services.
+
+### Language Selections
+
+GitLab is being translated into many languages.
+
+1. Select the language you would like to contribute translations to by clicking the flag
+1. You will see a list of files and folders.
+ Click `gitlab.pot` to open the translation editor.
+
+### Translation Editor
+
+The online translation editor is the easiest way to contribute translations.
+
+![Crowdin Editor](img/crowdin-editor.png)
+
+1. Strings for translation are listed in the left panel
+1. Translations are entered into the central panel.
+ Multiple translations will be required for strings that contains plurals.
+ The string to be translated is shown above with glossary terms highlighted.
+ If the string to be translated is not clear, you can 'Request Context'
+
+A glossary of common terms is available in the right panel by clicking Terms.
+Comments can be added to discuss a translation with the community.
+
+Remember to **Save** each translation.
+
+## Translation Guidelines
+
+Be sure to check the following guidelines before you translate any strings.
+
+### Technical terms
+
+Technical terms should be treated like proper nouns and not be translated.
+This helps maintain a logical connection and consistency between tools (e.g. `git` client) and
+GitLab.
+
+Technical terms that should always be in English are noted in the glossary when using
+[translate.gitlab.com](https://translate.gitlab.com).
+
+### Formality
+
+The level of formality used in software varies by language.
+For example, in French we translate `you` as the informal `tu`.
+
+You can refer to other translated strings and notes in the glossary to assist determining a
+suitable level of formality.
+
+### Updating the glossary
+
+To propose additions to the glossary please
+[open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues).
diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md
index 29c8941a8f7..f6e949b5fd8 100644
--- a/doc/development/i18n_guide.md
+++ b/doc/development/i18n_guide.md
@@ -1,304 +1 @@
-# Internationalization for GitLab
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10669) in GitLab 9.2.
-
-For working with internationalization (i18n) we use
-[GNU gettext](https://www.gnu.org/software/gettext/) given it's the most used
-tool for this task and we have a lot of applications that will help us to work
-with it.
-
-## Setting up GitLab Development Kit (GDK)
-
-In order to be able to work on the [GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-ce) project we must download and
-configure it through [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit), we can do it by following this [guide](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/set-up-gdk.md).
-
-Once we have the GitLab project ready we can start working on the
-translation of the project.
-
-## Tools
-
-We use a couple of gems:
-
-1. [`gettext_i18n_rails`](https://github.com/grosser/gettext_i18n_rails): this
- gem allow us to translate content from models, views and controllers. Also
- it gives us access to the following raketasks:
- - `rake gettext:find`: Parses almost all the files from the
- Rails application looking for content that has been marked for
- translation. Finally, it updates the PO files with the new content that
- it has found.
- - `rake gettext:pack`: Processes the PO files and generates the
- MO files that are binary and are finally used by the application.
-
-1. [`gettext_i18n_rails_js`](https://github.com/webhippie/gettext_i18n_rails_js):
- this gem is useful to make the translations available in JavaScript. It
- provides the following raketask:
- - `rake gettext:po_to_json`: Reads the contents from the PO files and
- generates JSON files containing all the available translations.
-
-1. PO editor: there are multiple applications that can help us to work with PO
- files, a good option is [Poedit](https://poedit.net/download) which is
- available for macOS, GNU/Linux and Windows.
-
-## Preparing a page for translation
-
-We basically have 4 types of files:
-
-1. Ruby files: basically Models and Controllers.
-1. HAML files: these are the view files.
-1. ERB files: used for email templates.
-1. JavaScript files: we mostly need to work with VUE JS templates.
-
-### Ruby files
-
-If there is a method or variable that works with a raw string, for instance:
-
-```ruby
-def hello
- "Hello world!"
-end
-```
-
-Or:
-
-```ruby
-hello = "Hello world!"
-```
-
-You can easily mark that content for translation with:
-
-```ruby
-def hello
- _("Hello world!")
-end
-```
-
-Or:
-
-```ruby
-hello = _("Hello world!")
-```
-
-### HAML files
-
-Given the following content in HAML:
-
-```haml
-%h1 Hello world!
-```
-
-You can mark that content for translation with:
-
-```haml
-%h1= _("Hello world!")
-```
-
-### ERB files
-
-Given the following content in ERB:
-
-```erb
-<h1>Hello world!</h1>
-```
-
-You can mark that content for translation with:
-
-```erb
-<h1><%= _("Hello world!") %></h1>
-```
-
-### JavaScript files
-
-In JavaScript we added the `__()` (double underscore parenthesis) function
-for translations.
-
-### Updating the PO files with the new content
-
-Now that the new content is marked for translation, we need to update the PO
-files with the following command:
-
-```sh
-bundle exec rake gettext:find
-```
-
-This command will update the `locale/**/gitlab.edit.po` file with the
-new content that the parser has found.
-
-New translations will be added with their default content and will be marked
-fuzzy. To use the translation, look for the `#, fuzzy` mention in `gitlab.edit.po`
-and remove it.
-
-We need to make sure we remove the `fuzzy` translations before generating the
-`locale/**/gitlab.po` file. When they aren't removed, the resulting `.po` will
-be treated as a binary file which could overwrite translations that were merged
-before the new translations.
-
-When we are just preparing a page to be translated, but not actually adding any
-translations. There's no need to generate `.po` files.
-
-Translations that aren't used in the source code anymore will be marked with
-`~#`; these can be removed to keep our translation files clutter-free.
-
-### Validating PO files
-
-To make sure we keep our translation files up to date, there's a linter that is
-running on CI as part of the `static-analysis` job.
-
-To lint the adjustments in PO files locally you can run `rake gettext:lint`.
-
-The linter will take the following into account:
-
-- Valid PO-file syntax
-- Variable usage
- - Only one unnamed (`%d`) variable, since the order of variables might change
- in different languages
- - All variables used in the message-id are used in the translation
- - There should be no variables used in a translation that aren't in the
- message-id
-- Errors during translation.
-
-The errors are grouped per file, and per message ID:
-
-```
-Errors in `locale/zh_HK/gitlab.po`:
- PO-syntax errors
- SimplePoParser::ParserErrorSyntax error in lines
- Syntax error in msgctxt
- Syntax error in msgid
- Syntax error in msgstr
- Syntax error in message_line
- There should be only whitespace until the end of line after the double quote character of a message text.
- Parseing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
- SimplePoParser filtered backtrace: SimplePoParser::ParserError
-Errors in `locale/zh_TW/gitlab.po`:
- 1 pipeline
- <%d 條流水線> is using unknown variables: [%d]
- Failure translating to zh_TW with []: too few arguments
-```
-
-In this output the `locale/zh_HK/gitlab.po` has syntax errors.
-The `locale/zh_TW/gitlab.po` has variables that are used in the translation that
-aren't in the message with id `1 pipeline`.
-
-## Working with special content
-
-### Interpolation
-
-- In Ruby/HAML (see [sprintf]):
-
- ```ruby
- _("Hello %{name}") % { name: 'Joe' }
- ```
-
-- In JavaScript: Only named parameters are supported (see also [#37992]):
-
- ```javascript
- __("Hello %{name}") % { name: 'Joe' }
- ```
-
-[sprintf]: http://ruby-doc.org/core/Kernel.html#method-i-sprintf
-[#37992]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
-
-### Plurals
-
-- In Ruby/HAML:
-
- ```ruby
- n_('Apple', 'Apples', 3) => 'Apples'
- ```
-
- Using interpolation:
- ```ruby
- n_("There is a mouse.", "There are %d mice.", size) % size
- ```
-
-- In JavaScript:
-
- ```js
- n__('Apple', 'Apples', 3) => 'Apples'
- ```
-
- Using interpolation:
-
- ```js
- n__('Last day', 'Last %d days', 30) => 'Last 30 days'
- ```
-
-### Namespaces
-
-Sometimes you need to add some context to the text that you want to translate
-(if the word occurs in a sentence and/or the word is ambiguous).
-
-- In Ruby/HAML:
-
- ```ruby
- s_('OpenedNDaysAgo|Opened')
- ```
-
- In case the translation is not found it will return `Opened`.
-
-- In JavaScript:
-
- ```js
- s__('OpenedNDaysAgo|Opened')
- ```
-
-### Just marking content for parsing
-
-Sometimes there are some dynamic translations that can't be found by the
-parser when running `bundle exec rake gettext:find`. For these scenarios you can
-use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
-
-There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
-
-## Adding a new language
-
-Let's suppose you want to add translations for a new language, let's say French.
-
-1. The first step is to register the new language in `lib/gitlab/i18n.rb`:
-
- ```ruby
- ...
- AVAILABLE_LANGUAGES = {
- ...,
- 'fr' => 'Français'
- }.freeze
- ...
- ```
-
-1. Next, you need to add the language:
-
- ```sh
- bundle exec rake gettext:add_language[fr]
- ```
-
- If you want to add a new language for a specific region, the command is similar,
- you just need to separate the region with an underscore (`_`). For example:
-
- ```sh
- bundle exec rake gettext:add_language[en_GB]
- ```
-
- Please note that you need to specify the region part in capitals.
-
-1. Now that the language is added, a new directory has been created under the
- path: `locale/fr/`. You can now start using your PO editor to edit the PO file
- located in: `locale/fr/gitlab.edit.po`.
-
-1. After you're done updating the translations, you need to process the PO files
- in order to generate the binary MO files and finally update the JSON files
- containing the translations:
-
- ```sh
- bundle exec rake gettext:compile
- ```
-
-1. In order to see the translated content we need to change our preferred language
- which can be found under the user's **Settings** (`/profile`).
-
-1. After checking that the changes are ok, you can proceed to commit the new files.
- For example:
-
- ```sh
- git add locale/fr/ app/assets/javascripts/locale/fr/
- git commit -m "Add French translations for Cycle Analytics page"
- ```
+This document was moved to [a new location](i18n/index.md).
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index 986b796437b..fa31c496b30 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -42,6 +42,37 @@ By default, tooltips should be placed below the referring element. However, if t
---
+## Popovers
+
+Popovers provide additional, useful, unique information about the referring elements and can provide one or multiple actionable elements. They inform the user of additional information within the context of their original view, but without forcing the user to act upon it like a modal. Popovers are different from tooltips, which do not provide rich markup and actionable items. A popover can contain a header section with a different background color.
+
+Popovers are summoned:
+
+* Upon hover or touch on an element
+
+### Usage
+A popover should be used:
+* When you don't want to let the user lose context, but still want to provide additional useful unique information about referring elements
+* When it isn’t critical for the user to act upon the information
+* When you want to give a user a summary of extended information and the option to switch context if they want to dive in deeper.
+
+### Styling
+
+A popover can contain a header section with a different background color if that improves readability and separation of content within.
+
+![Popover usage](img/popover-placement-below.png)
+
+This example shows two sections, where each section includes an actionable element. The first section shows a summary of the content shown when clicking the "read more" link. With this information the user can decide to dive deeper or start their GitLab Enterprise Edition trial immediately.
+
+### Placement
+By default, tooltips should be placed below the referring element. However, if there isn’t enough space in the viewport or it blocks related content, the tooltip should be moved to the side or above as needed.
+
+![Tooltip placement location](img/popover-placement-above.png)
+
+In this example we let the user know more about the setting they are deciding over, without loosing context. If they want to know even more they can do so, but with the expectation of opening that content in a new view.
+
+---
+
## Anchor links
Anchor links are used for navigational actions and lone, secondary commands (such as 'Reset filters' on the Issues List) when deemed appropriate by the UX team.
diff --git a/doc/development/ux_guide/img/illustration-size-large-horizontal.png b/doc/development/ux_guide/img/illustration-size-large-horizontal.png
index 8aa835adccc..8aa835adccc 100755..100644
--- a/doc/development/ux_guide/img/illustration-size-large-horizontal.png
+++ b/doc/development/ux_guide/img/illustration-size-large-horizontal.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustration-size-medium.png b/doc/development/ux_guide/img/illustration-size-medium.png
index 55cfe1dcb91..55cfe1dcb91 100755..100644
--- a/doc/development/ux_guide/img/illustration-size-medium.png
+++ b/doc/development/ux_guide/img/illustration-size-medium.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-border-radius.png b/doc/development/ux_guide/img/illustrations-border-radius.png
index 4e2fef5c7f5..4e2fef5c7f5 100755..100644
--- a/doc/development/ux_guide/img/illustrations-border-radius.png
+++ b/doc/development/ux_guide/img/illustrations-border-radius.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-caps-do.png b/doc/development/ux_guide/img/illustrations-caps-do.png
index 7a2c74382f6..7a2c74382f6 100755..100644
--- a/doc/development/ux_guide/img/illustrations-caps-do.png
+++ b/doc/development/ux_guide/img/illustrations-caps-do.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-caps-don't.png b/doc/development/ux_guide/img/illustrations-caps-don't.png
index 848f72dbe30..848f72dbe30 100755..100644
--- a/doc/development/ux_guide/img/illustrations-caps-don't.png
+++ b/doc/development/ux_guide/img/illustrations-caps-don't.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-color-grey.png b/doc/development/ux_guide/img/illustrations-color-grey.png
index 63855026c2b..63855026c2b 100755..100644
--- a/doc/development/ux_guide/img/illustrations-color-grey.png
+++ b/doc/development/ux_guide/img/illustrations-color-grey.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-color-orange.png b/doc/development/ux_guide/img/illustrations-color-orange.png
index 96765c8c28c..96765c8c28c 100755..100644
--- a/doc/development/ux_guide/img/illustrations-color-orange.png
+++ b/doc/development/ux_guide/img/illustrations-color-orange.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-color-purple.png b/doc/development/ux_guide/img/illustrations-color-purple.png
index 745d2c853ba..745d2c853ba 100755..100644
--- a/doc/development/ux_guide/img/illustrations-color-purple.png
+++ b/doc/development/ux_guide/img/illustrations-color-purple.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-geometric.png b/doc/development/ux_guide/img/illustrations-geometric.png
index 33f05547bac..33f05547bac 100755..100644
--- a/doc/development/ux_guide/img/illustrations-geometric.png
+++ b/doc/development/ux_guide/img/illustrations-geometric.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-palette-oragne.png b/doc/development/ux_guide/img/illustrations-palette-oragne.png
index 15f35912646..15f35912646 100755..100644
--- a/doc/development/ux_guide/img/illustrations-palette-oragne.png
+++ b/doc/development/ux_guide/img/illustrations-palette-oragne.png
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-palette-purple.png b/doc/development/ux_guide/img/illustrations-palette-purple.png
index e0f5839705e..e0f5839705e 100755..100644
--- a/doc/development/ux_guide/img/illustrations-palette-purple.png
+++ b/doc/development/ux_guide/img/illustrations-palette-purple.png
Binary files differ
diff --git a/doc/development/ux_guide/img/popover-placement-above.png b/doc/development/ux_guide/img/popover-placement-above.png
new file mode 100644
index 00000000000..1aa044bfc9c
--- /dev/null
+++ b/doc/development/ux_guide/img/popover-placement-above.png
Binary files differ
diff --git a/doc/development/ux_guide/img/popover-placement-below.png b/doc/development/ux_guide/img/popover-placement-below.png
new file mode 100644
index 00000000000..2d6ab8a1618
--- /dev/null
+++ b/doc/development/ux_guide/img/popover-placement-below.png
Binary files differ
diff --git a/doc/development/verifying_database_capabilities.md b/doc/development/verifying_database_capabilities.md
index cc6d62957e3..ffdeff47d4a 100644
--- a/doc/development/verifying_database_capabilities.md
+++ b/doc/development/verifying_database_capabilities.md
@@ -24,3 +24,15 @@ else
run_query
end
```
+
+# Read-only database
+
+The database can be used in read-only mode. In this case we have to
+make sure all GET requests don't attempt any write operations to the
+database. If one of those requests wants to write to the database, it needs
+to be wrapped in a `Gitlab::Database.read_only?` or `Gitlab::Database.read_write?`
+guard, to make sure it doesn't for read-only databases.
+
+We have a Rails Middleware that filters any potentially writing
+operations (the CUD operations of CRUD) and prevent the user from trying
+to update the database and getting a 500 error (see `Gitlab::Middleware::ReadOnly`).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 200cd94f43c..af6c797dc00 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -133,9 +133,9 @@ Remove the old Ruby 1.8 if present:
Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
- curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
- echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
- cd ruby-2.3.3
+ curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz
+ echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz
+ cd ruby-2.3.5
./configure --disable-install-rdoc
make
sudo make install
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index ae69d7f92f2..e4c09b2b507 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -370,7 +370,7 @@ This is recommended to reduce cron spam.
## Restore
-GitLab provides a simple command line interface to backup your whole installation,
+GitLab provides a simple command line interface to restore your whole installation,
and is flexible enough to fit your needs.
The [restore prerequisites section](#restore-prerequisites) includes crucial
@@ -445,6 +445,14 @@ Restoring repositories:
Deleting tmp directories...[DONE]
```
+Next, restore `/home/git/gitlab/.secret` if necessary as mentioned above.
+
+Restart GitLab:
+
+```shell
+sudo service gitlab restart
+```
+
### Restore for Omnibus installations
This procedure assumes that:
@@ -480,10 +488,12 @@ restore:
sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2017_04_25_9.1.0
```
+Next, restore `/etc/gitlab/gitlab-secrets.json` if necessary as mentioned above.
+
Restart and check GitLab:
```shell
-sudo gitlab-ctl start
+sudo gitlab-ctl restart
sudo gitlab-rake gitlab:check SANITIZE=true
```
diff --git a/doc/update/10.0-to-10.1.md b/doc/update/10.0-to-10.1.md
new file mode 100644
index 00000000000..4a9384f3ad6
--- /dev/null
+++ b/doc/update/10.0-to-10.1.md
@@ -0,0 +1,356 @@
+# From 10.0 to 10.1
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz
+echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz
+cd ruby-2.3.5
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-1-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-1-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell'.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-0-stable:config/gitlab.yml.example origin/10-1-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/10-0-stable:lib/support/nginx/gitlab-ssl origin/10-1-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/10-0-stable:lib/support/nginx/gitlab origin/10-1-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-1-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-1-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-0-stable:lib/support/init.d/gitlab.default.example origin/10-1-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 12. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 13. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 14. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (9.5)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 9.4 to 9.5](9.4-to-9.5.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-1-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-1-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index 5dc8e6f65f8..fff47180099 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -1,80 +1,267 @@
-*** NOTE: These instructions should be considered deprecated. In GitLab 10.0 we will be releasing new migration instructions using [pgloader](http://pgloader.io/).
+---
+last_updated: 2017-10-05
+---
-# Migrating GitLab from MySQL to Postgres
-*Make sure you view this [guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/mysql_to_postgresql.md#migrating-gitlab-from-mysql-to-postgres) for the most up to date instructions.*
+# Migrating from MySQL to PostgreSQL
-If you are replacing MySQL with Postgres while keeping GitLab on the same server all you need to do is to export from MySQL, convert the resulting SQL file, and import it into Postgres. If you are also moving GitLab to another server, or if you are switching to omnibus-gitlab, you may want to use a GitLab backup file. The second part of this documents explains the procedure to do this.
+> **Note:** This guide assumes you have a working Omnibus GitLab instance with
+> MySQL and want to migrate to bundled PostgreSQL database.
-## Export from MySQL and import into Postgres
+## Prerequisites
-Use this if you are keeping GitLab on the same server.
+First, we'll need to enable the bundled PostgreSQL database with up-to-date
+schema. Next, we'll use [pgloader](http://pgloader.io) to migrate the data
+from the old MySQL database to the new PostgreSQL one.
-```
-sudo service gitlab stop
+Here's what you'll need to have installed:
-# Update /home/git/gitlab/config/database.yml
+- pgloader 3.4.1+
+- Omnibus GitLab
+- MySQL
-git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab
-cd mysql-postgresql-converter
-mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production -p
-python db_converter.py gitlabhq_production.mysql gitlabhq_production.psql
-ed -s gitlabhq_production.psql < move_drop_indexes.ed
+## Enable bundled PostgreSQL database
-# Import the database dump as the application database user
-sudo -u git psql -f gitlabhq_production.psql -d gitlabhq_production
+1. Stop GitLab:
-# Install gems for PostgreSQL (note: the line below states '--without ... mysql')
-sudo -u git -H bundle install --without development test mysql --deployment
+ ``` bash
+ sudo gitlab-ctl stop
+ ```
-sudo service gitlab start
-```
+1. Edit `/etc/gitlab/gitlab.rb` to enable bundled PostgreSQL:
-## Converting a GitLab backup file from MySQL to Postgres
-**Note:** Please make sure to have Python 2.7.x (or higher) installed.
+ ```
+ postgresql['enable'] = true
+ ```
-GitLab backup files (`<timestamp>_gitlab_backup.tar`) contain a SQL dump. Using the lanyrd database converter we can replace a MySQL database dump inside the tar file with a Postgres database dump. This can be useful if you are moving to another server.
+1. Edit `/etc/gitlab/gitlab.rb` to use the bundled PostgreSQL. Please check
+ all the settings beginning with `db_`, such as `gitlab_rails['db_adapter']`
+ and alike. You could just comment all of them out so that we'll just use
+ the defaults.
-```
-# Stop GitLab
-sudo service gitlab stop
+1. [Reconfigure GitLab] for the changes to take effect:
+
+ ``` bash
+ sudo gitlab-ctl reconfigure
+ ```
+
+1. Start Unicorn and PostgreSQL so that we can prepare the schema:
+
+ ``` bash
+ sudo gitlab-ctl start unicorn
+ sudo gitlab-ctl start postgresql
+ ```
+
+1. Run the following commands to prepare the schema:
+
+ ``` bash
+ sudo gitlab-rake db:create db:migrate
+ ```
+
+1. Stop Unicorn to prevent other database access from interfering with the loading of data:
+
+ ``` bash
+ sudo gitlab-ctl stop unicorn
+ ```
+
+After these steps, you'll have a fresh PostgreSQL database with up-to-date schema.
+
+## Migrate data from MySQL to PostgreSQL
+
+Now, you can use pgloader to migrate the data from MySQL to PostgreSQL:
-# Create the backup
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+1. Save the following snippet in a `commands.load` file, and edit with your
+ database `username`, `password` and `host`:
-# Note the filename of the backup that was created. We will call it
-# TIMESTAMP_gitlab_backup.tar below.
+ ```
+ LOAD DATABASE
+ FROM mysql://username:password@host/gitlabhq_production
+ INTO postgresql://gitlab-psql@unix://var/opt/gitlab/postgresql:/gitlabhq_production
-# Move the backup file we will convert to its own directory
-sudo -u git -H mkdir -p tmp/backups/postgresql
-sudo -u git -H mv tmp/backups/TIMESTAMP_gitlab_backup.tar tmp/backups/postgresql/
+ WITH include no drop, truncate, disable triggers, create no tables,
+ create no indexes, preserve index names, no foreign keys,
+ data only
-# Create a separate database dump with PostgreSQL compatibility
-cd tmp/backups/postgresql
-sudo -u git -H mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production -p
+ ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public'
-# Clone the database converter
-sudo -u git -H git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab
+ ;
+ ```
-# Convert gitlabhq_production.mysql
-sudo -u git -H mkdir db
-sudo -u git -H python mysql-postgresql-converter/db_converter.py gitlabhq_production.mysql db/database.sql
-sudo -u git -H ed -s db/database.sql < mysql-postgresql-converter/move_drop_indexes.ed
+1. Start the migration:
-# Compress database backup
-# Warning: If you have Gitlab 7.12.0 or older skip this step and import the database.sql directly into the backup with:
-# sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql
-# The compressed databasedump is not supported at 7.12.0 and older.
-sudo -u git -H gzip db/database.sql
+ ``` bash
+ sudo -u gitlab-psql pgloader commands.load
+ ```
-# Replace the MySQL dump in TIMESTAMP_gitlab_backup.tar.
+1. Once the migration finishes, you should see a summary table that looks like
+the following:
-# Warning: if you forget to replace TIMESTAMP below, tar will create a new file
-# 'TIMESTAMP_gitlab_backup.tar' without giving an error.
-sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql.gz
+ ```
+ table name read imported errors total time
+ ----------------------------------------------- --------- --------- --------- --------------
+ fetch meta data 119 119 0 0.388s
+ Truncate 119 119 0 1.134s
+ ----------------------------------------------- --------- --------- --------- --------------
+ public.abuse_reports 0 0 0 0.490s
+ public.appearances 0 0 0 0.488s
+ public.approvals 0 0 0 0.273s
+ public.application_settings 1 1 0 0.266s
+ public.approvers 0 0 0 0.339s
+ public.approver_groups 0 0 0 0.357s
+ public.audit_events 1 1 0 0.410s
+ public.award_emoji 0 0 0 0.441s
+ public.boards 0 0 0 0.505s
+ public.broadcast_messages 0 0 0 0.498s
+ public.chat_names 0 0 0 0.576s
+ public.chat_teams 0 0 0 0.617s
+ public.ci_builds 0 0 0 0.611s
+ public.ci_group_variables 0 0 0 0.620s
+ public.ci_pipelines 0 0 0 0.599s
+ public.ci_pipeline_schedules 0 0 0 0.622s
+ public.ci_pipeline_schedule_variables 0 0 0 0.573s
+ public.ci_pipeline_variables 0 0 0 0.594s
+ public.ci_runners 0 0 0 0.533s
+ public.ci_runner_projects 0 0 0 0.584s
+ public.ci_sources_pipelines 0 0 0 0.564s
+ public.ci_stages 0 0 0 0.595s
+ public.ci_triggers 0 0 0 0.569s
+ public.ci_trigger_requests 0 0 0 0.596s
+ public.ci_variables 0 0 0 0.565s
+ public.container_repositories 0 0 0 0.605s
+ public.conversational_development_index_metrics 0 0 0 0.571s
+ public.deployments 0 0 0 0.607s
+ public.emails 0 0 0 0.602s
+ public.deploy_keys_projects 0 0 0 0.557s
+ public.events 160 160 0 0.677s
+ public.environments 0 0 0 0.567s
+ public.features 0 0 0 0.639s
+ public.events_for_migration 160 160 0 0.582s
+ public.feature_gates 0 0 0 0.579s
+ public.forked_project_links 0 0 0 0.660s
+ public.geo_nodes 0 0 0 0.686s
+ public.geo_event_log 0 0 0 0.626s
+ public.geo_repositories_changed_events 0 0 0 0.677s
+ public.geo_node_namespace_links 0 0 0 0.618s
+ public.geo_repository_renamed_events 0 0 0 0.696s
+ public.gpg_keys 0 0 0 0.704s
+ public.geo_repository_deleted_events 0 0 0 0.638s
+ public.historical_data 0 0 0 0.729s
+ public.geo_repository_updated_events 0 0 0 0.634s
+ public.index_statuses 0 0 0 0.746s
+ public.gpg_signatures 0 0 0 0.667s
+ public.issue_assignees 80 80 0 0.769s
+ public.identities 0 0 0 0.655s
+ public.issue_metrics 80 80 0 0.781s
+ public.issues 80 80 0 0.720s
+ public.labels 0 0 0 0.795s
+ public.issue_links 0 0 0 0.707s
+ public.label_priorities 0 0 0 0.793s
+ public.keys 0 0 0 0.734s
+ public.lfs_objects 0 0 0 0.812s
+ public.label_links 0 0 0 0.725s
+ public.licenses 0 0 0 0.813s
+ public.ldap_group_links 0 0 0 0.751s
+ public.members 52 52 0 0.830s
+ public.lfs_objects_projects 0 0 0 0.738s
+ public.merge_requests_closing_issues 0 0 0 0.825s
+ public.lists 0 0 0 0.769s
+ public.merge_request_diff_commits 0 0 0 0.840s
+ public.merge_request_metrics 0 0 0 0.837s
+ public.merge_requests 0 0 0 0.753s
+ public.merge_request_diffs 0 0 0 0.771s
+ public.namespaces 30 30 0 0.874s
+ public.merge_request_diff_files 0 0 0 0.775s
+ public.notes 0 0 0 0.849s
+ public.milestones 40 40 0 0.799s
+ public.oauth_access_grants 0 0 0 0.979s
+ public.namespace_statistics 0 0 0 0.797s
+ public.oauth_applications 0 0 0 0.899s
+ public.notification_settings 72 72 0 0.818s
+ public.oauth_access_tokens 0 0 0 0.807s
+ public.pages_domains 0 0 0 0.958s
+ public.oauth_openid_requests 0 0 0 0.832s
+ public.personal_access_tokens 0 0 0 0.965s
+ public.projects 8 8 0 0.987s
+ public.path_locks 0 0 0 0.925s
+ public.plans 0 0 0 0.923s
+ public.project_features 8 8 0 0.985s
+ public.project_authorizations 66 66 0 0.969s
+ public.project_import_data 8 8 0 1.002s
+ public.project_statistics 8 8 0 1.001s
+ public.project_group_links 0 0 0 0.949s
+ public.project_mirror_data 0 0 0 0.972s
+ public.protected_branch_merge_access_levels 0 0 0 1.017s
+ public.protected_branches 0 0 0 0.969s
+ public.protected_branch_push_access_levels 0 0 0 0.991s
+ public.protected_tags 0 0 0 1.009s
+ public.protected_tag_create_access_levels 0 0 0 0.985s
+ public.push_event_payloads 0 0 0 1.041s
+ public.push_rules 0 0 0 0.999s
+ public.redirect_routes 0 0 0 1.020s
+ public.remote_mirrors 0 0 0 1.034s
+ public.releases 0 0 0 0.993s
+ public.schema_migrations 896 896 0 1.057s
+ public.routes 38 38 0 1.021s
+ public.services 0 0 0 1.055s
+ public.sent_notifications 0 0 0 1.003s
+ public.slack_integrations 0 0 0 1.022s
+ public.spam_logs 0 0 0 1.024s
+ public.snippets 0 0 0 1.058s
+ public.subscriptions 0 0 0 1.069s
+ public.taggings 0 0 0 1.099s
+ public.timelogs 0 0 0 1.104s
+ public.system_note_metadata 0 0 0 1.038s
+ public.tags 0 0 0 1.034s
+ public.trending_projects 0 0 0 1.140s
+ public.uploads 0 0 0 1.129s
+ public.todos 80 80 0 1.085s
+ public.users_star_projects 0 0 0 1.153s
+ public.u2f_registrations 0 0 0 1.061s
+ public.web_hooks 0 0 0 1.179s
+ public.users 26 26 0 1.163s
+ public.user_agent_details 0 0 0 1.068s
+ public.web_hook_logs 0 0 0 1.080s
+ ----------------------------------------------- --------- --------- --------- --------------
+ COPY Threads Completion 4 4 0 2.008s
+ Reset Sequences 113 113 0 0.304s
+ Install Comments 0 0 0 0.000s
+ ----------------------------------------------- --------- --------- --------- --------------
+ Total import time 1894 1894 0 12.497s
+ ```
+
+ If there is no output for more than 30 minutes, it's possible pgloader encountered an error. See
+ the [troubleshooting guide](#Troubleshooting) for more details.
+
+1. Start GitLab:
+
+ ``` bash
+ sudo gitlab-ctl start
+ ```
+
+Now, you can verify that everything worked by visiting GitLab.
+
+## Troubleshooting
+
+### Permissions
+
+Note that the PostgreSQL user that you use for the above MUST have **superuser** privileges. Otherwise, you may see
+a similar message to the following:
-# Done! TIMESTAMP_gitlab_backup.tar can now be restored into a Postgres GitLab
-# installation.
-# See https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/raketasks/backup_restore.md for more information about backups.
```
+debugger invoked on a CL-POSTGRES-ERROR:INSUFFICIENT-PRIVILEGE in thread
+ #<THREAD "lparallel" RUNNING {10078A3513}>:
+ Database error 42501: permission denied: "RI_ConstraintTrigger_a_20937" is a system trigger
+ QUERY: ALTER TABLE ci_builds DISABLE TRIGGER ALL;
+ 2017-08-23T00:36:56.782000Z ERROR Database error 42501: permission denied: "RI_ConstraintTrigger_c_20864" is a system trigger
+ QUERY: ALTER TABLE approver_groups DISABLE TRIGGER ALL;
+```
+
+### Experiencing 500 errors after the migration
+
+If you experience 500 errors after the migration, try to clear the cache:
+
+``` bash
+sudo gitlab-rake cache:clear
+```
+
+[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/user/discussions/img/discussion_lock_system_notes.png b/doc/user/discussions/img/discussion_lock_system_notes.png
new file mode 100644
index 00000000000..8e8e8e0bc3d
--- /dev/null
+++ b/doc/user/discussions/img/discussion_lock_system_notes.png
Binary files differ
diff --git a/doc/user/discussions/img/lock_form_member.png b/doc/user/discussions/img/lock_form_member.png
new file mode 100644
index 00000000000..01c6308d24c
--- /dev/null
+++ b/doc/user/discussions/img/lock_form_member.png
Binary files differ
diff --git a/doc/user/discussions/img/lock_form_non_member.png b/doc/user/discussions/img/lock_form_non_member.png
new file mode 100644
index 00000000000..3bb70b69580
--- /dev/null
+++ b/doc/user/discussions/img/lock_form_non_member.png
Binary files differ
diff --git a/doc/user/discussions/img/turn_off_lock.png b/doc/user/discussions/img/turn_off_lock.png
new file mode 100644
index 00000000000..dd05b398a8b
--- /dev/null
+++ b/doc/user/discussions/img/turn_off_lock.png
Binary files differ
diff --git a/doc/user/discussions/img/turn_on_lock.png b/doc/user/discussions/img/turn_on_lock.png
new file mode 100644
index 00000000000..9597da4e14d
--- /dev/null
+++ b/doc/user/discussions/img/turn_on_lock.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index efea99eb120..ab46befb91c 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -153,12 +153,52 @@ comments in greater detail.
![Discussion comment](img/discussion_comment.png)
+## Locking discussions
+
+> [Introduced][ce-14531] in GitLab 10.1.
+
+There might be some cases where a discussion is better off if it's locked down.
+For example:
+
+- Discussions that are several years old and the issue/merge request is closed,
+ but people continue to try to resurrect the discussion.
+- Discussions where someone or a group of people are trolling, are abusive, or
+ in-general are causing the discussion to be unproductive.
+
+In locked discussions, only team members can write new comments and edit the old
+ones.
+
+To lock or unlock a discussion, you need to have at least Master [permissions]:
+
+1. Find the "Lock" section in the sidebar and click **Edit**
+1. In the dialog that will appear, you can choose to turn on or turn off the
+ discussion lock
+1. Optionally, leave a comment to explain your reasoning behind that action
+
+| Turn off discussion lock | Turn on discussion lock |
+| :-----------: | :----------: |
+| ![Turn off discussion lock](img/turn_off_lock.png) | ![Turn on discussion lock](img/turn_on_lock.png) |
+
+Every change is indicated by a system note in the issue's or merge request's
+comments.
+
+![Discussion lock system notes](img/discussion_lock_system_notes.png)
+
+Once an issue or merge request is locked, project members can see the indicator
+in the comment area, whereas non project members can only see the information
+that the discussion is locked.
+
+| Team member | Not a member |
+| :-----------: | :----------: |
+| ![Comment form member](img/lock_form_member.png) | ![Comment form non-member](img/lock_form_non_member.png) |
+
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
[ce-7527]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7527
[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
[ce-14053]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14053
+[ce-14531]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14531
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
[discussion-view]: img/discussion_view.png
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 44ee994a26b..be72d42625a 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -25,6 +25,7 @@ The following table depicts the various user permission levels in a project.
| Create confidential issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| Lock comments | | | | ✓ | ✓ |
| See a list of jobs | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
@@ -71,6 +72,7 @@ The following table depicts the various user permission levels in a project.
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
+| Delete issues | | | | | ✓ |
| Force push to protected branches [^4] | | | | | |
| Remove protected branches [^4] | | | | | |
| Remove pages | | | | | ✓ |
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index 016f98966e3..6423beefc77 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -42,6 +42,11 @@ The importer will create any new namespaces (groups) if they don't exist or in
the case the namespace is taken, the repository will be imported under the user's
namespace that started the import process.
+The importer will also import branches on forks of projects related to open pull
+requests. These branches will be imported with a naming scheume similar to
+GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepency
+in branches compared to the GitHub Repository.
+
## Importing your GitHub repositories
The importer page is visible when you create a new project.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 47eb0b34f66..7abc600a680 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -205,7 +205,7 @@ X-Gitlab-Event: Issue Hook
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
- "project":{
+ "project": {
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlabhq/gitlab-test",
@@ -221,7 +221,7 @@ X-Gitlab-Event: Issue Hook
"ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
"http_url":"http://example.com/gitlabhq/gitlab-test.git"
},
- "repository":{
+ "repository": {
"name": "Gitlab Test",
"url": "http://example.com/gitlabhq/gitlab-test.git",
"description": "Aut reprehenderit ut est.",
@@ -266,7 +266,37 @@ X-Gitlab-Event: Issue Hook
"description": "API related issues",
"type": "ProjectLabel",
"group_id": 41
- }]
+ }],
+ "changes": {
+ "updated_by_id": [null, 1],
+ "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"],
+ "labels": {
+ "previous": [{
+ "id": 206,
+ "title": "API",
+ "color": "#ffffff",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "API related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }],
+ "current": [{
+ "id": 205,
+ "title": "Platform",
+ "color": "#123123",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "Platform related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }]
+ }
+ }
}
```
@@ -661,6 +691,28 @@ X-Gitlab-Event: Merge Request Hook
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
+ "project": {
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlabhq/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
+ "namespace":"GitlabHQ",
+ "visibility_level":20,
+ "path_with_namespace":"gitlabhq/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlabhq/gitlab-test",
+ "url":"http://example.com/gitlabhq/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "http_url":"http://example.com/gitlabhq/gitlab-test.git"
+ },
+ "repository": {
+ "name": "Gitlab Test",
+ "url": "http://example.com/gitlabhq/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlabhq/gitlab-test"
+ },
"object_attributes": {
"id": 99,
"target_branch": "master",
@@ -679,7 +731,7 @@ X-Gitlab-Event: Merge Request Hook
"target_project_id": 14,
"iid": 1,
"description": "",
- "source":{
+ "source": {
"name":"Awesome Project",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/awesome_space/awesome_project",
@@ -729,6 +781,48 @@ X-Gitlab-Event: Merge Request Hook
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
}
+ },
+ "labels": [{
+ "id": 206,
+ "title": "API",
+ "color": "#ffffff",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "API related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }],
+ "changes": {
+ "updated_by_id": [null, 1],
+ "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"],
+ "labels": {
+ "previous": [{
+ "id": 206,
+ "title": "API",
+ "color": "#ffffff",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "API related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }],
+ "current": [{
+ "id": 205,
+ "title": "Platform",
+ "color": "#123123",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "Platform related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }]
+ }
}
}
```
@@ -1015,7 +1109,7 @@ X-Gitlab-Event: Build Hook
## Testing webhooks
-You can trigger the webhook manually. Sample data from the project will be used.Sample data will take from the project.
+You can trigger the webhook manually. Sample data from the project will be used.Sample data will take from the project.
> For example: for triggering `Push Events` your project should have at least one commit.
![Webhook testing](img/webhook_testing.png)
diff --git a/doc/user/project/issues/deleting_issues.md b/doc/user/project/issues/deleting_issues.md
new file mode 100644
index 00000000000..d7442104c53
--- /dev/null
+++ b/doc/user/project/issues/deleting_issues.md
@@ -0,0 +1,11 @@
+# Deleting Issues
+
+> [Introduced][ce-2982] in GitLab 8.6
+
+Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
+
+You can delete an issue by editing it and clicking on the delete button.
+
+![delete issue - button](img/delete_issue.png)
+
+>**Note:** Only [project owners](../../permissions.md) can delete issues. \ No newline at end of file
diff --git a/doc/user/project/issues/img/delete_issue.png b/doc/user/project/issues/img/delete_issue.png
new file mode 100644
index 00000000000..a356f52044e
--- /dev/null
+++ b/doc/user/project/issues/img/delete_issue.png
Binary files differ
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 0f187946a4a..3e81dcb78c6 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -90,6 +90,10 @@ Learn distinct ways to [close issues](closing_issues.md) in GitLab.
Read through the [documentation on moving issues](moving_issues.md).
+## Deleting issues
+
+Read through the [documentation on deleting issues](deleting_issues.md)
+
## Create a merge request from an issue
Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md#18-new-merge-request).
diff --git a/doc/user/project/pipelines/img/job_artifacts_browser.png b/doc/user/project/pipelines/img/job_artifacts_browser.png
index 145fe156bbb..d3d8de5ac60 100644
--- a/doc/user/project/pipelines/img/job_artifacts_browser.png
+++ b/doc/user/project/pipelines/img/job_artifacts_browser.png
Binary files differ
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index 4e93e680fd2..9ef6f9185c9 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -50,6 +50,10 @@ For more examples on artifacts, follow the [artifacts reference in
With GitLab 9.2, PDFs, images, videos and other formats can be previewed
directly in the job artifacts browser without the need to download them.
+>**Note:**
+With [GitLab 10.1][ce-14399], HTML files in a public project can be previewed
+directly in a new tab without the need to download them.
+
After a job finishes, if you visit the job's specific page, there are three
buttons. You can download the artifacts archive or browse its contents, whereas
the **Keep** button appears only if you have set an [expiry date] to the
@@ -64,7 +68,8 @@ archive. If your artifacts contained directories, then you are also able to
browse inside them.
Below you can see how browsing looks like. In this case we have browsed inside
-the archive and at this point there is one directory and one HTML file.
+the archive and at this point there is one directory, a couple files, and
+one HTML file that you can view directly online (opens in a new tab).
![Job artifacts browser](img/job_artifacts_browser.png)
@@ -158,3 +163,4 @@ information in the UI.
[expiry date]: ../../../ci/yaml/README.md#artifacts-expire_in
+[ce-14399]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14399
diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md
index 29e04a0ccf0..6b9976d133c 100644
--- a/doc/user/project/repository/gpg_signed_commits/index.md
+++ b/doc/user/project/repository/gpg_signed_commits/index.md
@@ -26,7 +26,7 @@ to be uploaded to GitLab. For a signature to be verified three conditions need
to be met:
1. The public key needs to be added your GitLab account
-1. One of the emails in the GPG key matches your **primary** email
+1. One of the emails in the GPG key matches a **verified** email address you use in GitLab
1. The committer's email matches the verified email from the gpg key
## Generating a GPG key
@@ -94,7 +94,7 @@ started:
```
1. Enter you real name, the email address to be associated with this key (should
- match the primary email address you use in GitLab) and an optional comment
+ match a verified email address you use in GitLab) and an optional comment
(press <kbd>Enter</kbd> to skip):
```
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
deleted file mode 100644
index 349fa2663a7..00000000000
--- a/features/project/merge_requests.feature
+++ /dev/null
@@ -1,324 +0,0 @@
-@project_merge_requests
-Feature: Project Merge Requests
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" have "Bug NS-04" open merge request
- And project "Shop" have "Feature NS-03" closed merge request
- And I visit project "Shop" merge requests page
-
- Scenario: I should see open merge requests
- Then I should see "Bug NS-04" in merge requests
- And I should not see "Feature NS-03" in merge requests
-
- Scenario: I should see CI status for merge requests
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- Given "Bug NS-05" has CI status
- When I visit project "Shop" merge requests page
- Then I should see merge request "Bug NS-05" with CI status
-
- Scenario: I should not see target branch name when it is project's default branch
- Then I should see "Bug NS-04" in merge requests
- And I should not see "master" branch
-
- Scenario: I should see target branch when it is different from default
- Given project "Shop" have "Bug NS-06" open merge request
- When I visit project "Shop" merge requests page
- Then I should see "feature_conflict" branch
-
- @javascript
- Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target
- Given project "Shop" have "Bug NS-07" open merge request with rebased branch
- When I visit merge request page "Bug NS-07"
- Then I should not see the diverged commits count
-
- @javascript
- Scenario: I should see the numbers of diverged commits if the branch diverged from the target
- Given project "Shop" have "Bug NS-08" open merge request with diverged branch
- When I visit merge request page "Bug NS-08"
- Then I should see the diverged commits count
-
- @javascript
- Scenario: I should see rejected merge requests
- Given I click link "Closed"
- Then I should see "Feature NS-03" in merge requests
- And I should not see "Bug NS-04" in merge requests
-
- @javascript
- Scenario: I should see all merge requests
- Given I click link "All"
- Then I should see "Feature NS-03" in merge requests
- And I should see "Bug NS-04" in merge requests
-
- @javascript
- Scenario: I visit an open merge request page
- Given I click link "Bug NS-04"
- Then I should see merge request "Bug NS-04"
-
- @javascript
- Scenario: I visit a merged merge request page
- Given project "Shop" have "Feature NS-05" merged merge request
- And I click link "Merged"
- And I click link "Feature NS-05"
- Then I should see merge request "Feature NS-05"
-
- @javascript
- Scenario: I close merge request page
- Given I click link "Bug NS-04"
- And I click link "Close"
- Then I should see closed merge request "Bug NS-04"
-
- @javascript
- Scenario: I reopen merge request page
- Given I click link "Bug NS-04"
- And I click link "Close"
- Then I should see closed merge request "Bug NS-04"
- When I click link "Reopen"
- Then I should see reopened merge request "Bug NS-04"
-
- @javascript
- Scenario: I submit new unassigned merge request
- Given I click link "New Merge Request"
- And I submit new merge request "Wiki Feature"
- Then I should see merge request "Wiki Feature"
-
- @javascript
- Scenario: I comment on a merge request
- Given I visit merge request page "Bug NS-04"
- And I leave a comment like "XML attached"
- Then I should see comment "XML attached"
-
- @javascript
- Scenario: Visiting Merge Requests after being sorted the list
- Given I visit project "Shop" merge requests page
- And I sort the list by "Last updated"
- And I visit my project's home page
- And I visit project "Shop" merge requests page
- Then The list should be sorted by "Last updated"
-
- @javascript
- Scenario: Visiting Merge Requests from a different Project after sorting
- Given I visit project "Shop" merge requests page
- And I sort the list by "Last updated"
- And I visit dashboard merge requests page
- Then The list should be sorted by "Last updated"
-
- @javascript
- Scenario: Sort merge requests by upvotes
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And project "Shop" have "Bug NS-06" open merge request
- And merge request "Bug NS-04" have 2 upvotes and 1 downvote
- And merge request "Bug NS-06" have 1 upvote and 2 downvotes
- And I sort the list by "Popularity"
- Then The list should be sorted by "Popularity"
-
- @javascript
- Scenario: I comment on a merge request diff
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is wrong" on diff
- And I switch to the merge request's comments tab
- Then I should see a discussion has started on diff
- And I should see a badge of "1" next to the discussion link
-
- @javascript
- Scenario: I see a new comment on merge request diff from another user in the discussion tab
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And user "John Doe" leaves a comment like "Line is wrong" on diff
- Then I should see a discussion by user "John Doe" has started on diff
- And I should see a badge of "1" next to the discussion link
-
- @javascript
- Scenario: I edit a comment on a merge request diff
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is wrong" on diff
- And I change the comment "Line is wrong" to "Typo, please fix" on diff
- Then I should not see a diff comment saying "Line is wrong"
- And I should see a diff comment saying "Typo, please fix"
-
- @javascript
- Scenario: I delete a comment on a merge request diff
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is wrong" on diff
- And I should see a badge of "1" next to the discussion link
- And I delete the comment "Line is wrong" on diff
- And I click on the Discussion tab
- Then I should not see any discussion
- And I should see a badge of "0" next to the discussion link
-
- @javascript
- Scenario: I comment on a line of a commit in merge request
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the commit in the merge request
- And I leave a comment like "Line is wrong" on diff in commit
- And I switch to the merge request's comments tab
- Then I should see a discussion has started on commit diff
-
- @javascript
- Scenario: I comment on a commit in merge request
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the commit in the merge request
- And I leave a comment on the diff page in commit
- And I switch to the merge request's comments tab
- Then I should see a discussion has started on commit
-
- @javascript
- Scenario: I accept merge request with custom commit message
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And merge request "Bug NS-05" is mergeable
- And I visit merge request page "Bug NS-05"
- And merge request is mergeable
- Then I modify merge commit message
- And I accept this merge request
- Then I should see merged request
-
- # Markdown
-
- @javascript
- Scenario: Headers inside the description should have ids generated for them.
- When I visit merge request page "Bug NS-04"
- Then Header "Description header" should have correct id and link
-
- @javascript
- Scenario: Headers inside comments should not have ids generated for them.
- Given I visit merge request page "Bug NS-04"
- And I leave a comment with a header containing "Comment with a header"
- Then The comment with the header should not have an ID
-
- # Toggling inline comments
-
- @javascript
- Scenario: I hide comments on a merge request diff with comments in a single file
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is wrong" on line 39 of the third file
- And I click link "Hide inline discussion" of the third file
- Then I should not see a comment like "Line is wrong here" in the third file
-
- @javascript
- Scenario: I show comments on a merge request diff with comments in a single file
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is wrong" on line 39 of the third file
- Then I should see a comment like "Line is wrong" in the third file
-
- @javascript
- Scenario: I hide comments on a merge request diff with comments in multiple files
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the second file
- And I leave a comment like "Line is wrong" on line 39 of the third file
- And I click link "Hide inline discussion" of the third file
- Then I should not see a comment like "Line is wrong here" in the third file
- And I should still see a comment like "Line is correct" in the second file
-
- @javascript
- Scenario: I show comments on a merge request diff with comments in multiple files
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the second file
- And I leave a comment like "Line is wrong" on line 39 of the third file
- And I click link "Hide inline discussion" of the third file
- And I click link "Show inline discussion" of the third file
- Then I should see a comment like "Line is wrong" in the third file
- And I should still see a comment like "Line is correct" in the second file
-
- @javascript
- Scenario: I unfold diff
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I unfold diff
- Then I should see additional file lines
-
- @javascript
- Scenario: I unfold diff in Side-by-Side view
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I click Side-by-side Diff tab
- And I unfold diff
- Then I should see additional file lines
-
- @javascript
- Scenario: I show comments on a merge request side-by-side diff with comments in multiple files
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the second file
- And I leave a comment like "Line is wrong" on line 39 of the third file
- And I click Side-by-side Diff tab
- Then I should see comments on the side-by-side diff page
-
- @javascript
- Scenario: I view diffs on a merge request
- Given project "Shop" have "Bug NS-05" open merge request with diffs inside
- And I visit merge request page "Bug NS-05"
- And I click on the Changes tab
- Then I should see the proper Inline and Side-by-side links
-
- # Description preview
-
- @javascript
- Scenario: I can't preview without text
- Given I visit merge request page "Bug NS-04"
- And I click link "Edit" for the merge request
- And I haven't written any description text
- Then The Markdown preview tab should say there is nothing to do
-
- @javascript
- Scenario: I can preview with text
- Given I visit merge request page "Bug NS-04"
- And I click link "Edit" for the merge request
- And I write a description like ":+1: Nice"
- Then The Markdown preview tab should display rendered Markdown
-
- @javascript
- Scenario: I preview a merge request description
- Given I visit merge request page "Bug NS-04"
- And I click link "Edit" for the merge request
- And I preview a description text like "Bug fixed :smile:"
- Then I should see the Markdown preview
- And I should not see the Markdown text field
-
- @javascript
- Scenario: I can edit after preview
- Given I visit merge request page "Bug NS-04"
- And I click link "Edit" for the merge request
- And I preview a description text like "Bug fixed :smile:"
- Then I should see the Markdown write tab
-
- @javascript
- Scenario: I can unsubscribe from merge request
- Given I visit merge request page "Bug NS-04"
- Then I should see that I am subscribed
- When I click button "Unsubscribe"
- Then I should see that I am unsubscribed
-
- @javascript
- Scenario: I can change the target branch
- Given I visit merge request page "Bug NS-04"
- And I click link "Edit" for the merge request
- When I click the "Target branch" dropdown
- And I select a new target branch
- Then I should see new target branch changes
-
- @javascript
- Scenario: I can close merge request after commenting
- Given I visit merge request page "Bug NS-04"
- And I leave a comment like "XML attached"
- Then I should see comment "XML attached"
- And I click link "Close"
- Then I should see closed merge request "Bug NS-04"
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 3490bbd968c..60707f26aee 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -58,13 +58,13 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'I should see my fork on the list' do
page.within('.js-projects-list-holder') do
- project = @user.fork_of(@project)
+ project = @user.fork_of(@project.reload)
expect(page).to have_content("#{project.namespace.human_name} / #{project.name}")
end
end
step 'I make forked repo invalid' do
- project = @user.fork_of(@project)
+ project = @user.fork_of(@project.reload)
project.path = 'test-crappy-path'
project.save!
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 420ac8a695a..6781a906a94 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -5,6 +5,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
include SharedPaths
include Select2Helper
include WaitForRequests
+ include ProjectForksHelper
step 'I am a member of project "Shop"' do
@project = ::Project.find_by(name: "Shop")
@@ -13,7 +14,9 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end
step 'I have a project forked off of "Shop" called "Forked Shop"' do
- @forked_project = Projects::ForkService.new(@project, @user).execute
+ @forked_project = fork_project(@project, @user,
+ namespace: @user.namespace,
+ repository: true)
end
step 'I click link "New Merge Request"' do
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
deleted file mode 100644
index dde918e3d41..00000000000
--- a/features/steps/project/merge_requests.rb
+++ /dev/null
@@ -1,632 +0,0 @@
-class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedProject
- include SharedNote
- include SharedPaths
- include SharedMarkdown
- include SharedDiffNote
- include SharedUser
- include WaitForRequests
-
- after do
- wait_for_requests if javascript_test?
- end
-
- step 'I click link "New Merge Request"' do
- page.within '.nav-controls' do
- page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
- end
- end
-
- step 'I click link "Bug NS-04"' do
- click_link "Bug NS-04"
- end
-
- step 'I click link "Feature NS-05"' do
- click_link "Feature NS-05"
- end
-
- step 'I click link "All"' do
- find('.issues-state-filters [data-state="all"] span', text: 'All').click
- # Waits for load
- expect(find('.issues-state-filters > .active')).to have_content 'All'
- end
-
- step 'I click link "Merged"' do
- find('#state-merged').trigger('click')
- end
-
- step 'I click link "Closed"' do
- find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click
- end
-
- step 'I should see merge request "Wiki Feature"' do
- page.within '.merge-request' do
- expect(page).to have_content "Wiki Feature"
- end
- wait_for_requests
- end
-
- step 'I should see closed merge request "Bug NS-04"' do
- expect(page).to have_content "Bug NS-04"
- expect(page).to have_content "Closed by"
- wait_for_requests
- end
-
- step 'I should see merge request "Bug NS-04"' do
- expect(page).to have_content "Bug NS-04"
- wait_for_requests
- end
-
- step 'I should see merge request "Feature NS-05"' do
- expect(page).to have_content "Feature NS-05"
- wait_for_requests
- end
-
- step 'I should not see "master" branch' do
- expect(find('.issuable-info')).not_to have_content "master"
- end
-
- step 'I should see "feature_conflict" branch' do
- expect(page).to have_content "feature_conflict"
- end
-
- step 'I should see "Bug NS-04" in merge requests' do
- expect(page).to have_content "Bug NS-04"
- end
-
- step 'I should see "Feature NS-03" in merge requests' do
- expect(page).to have_content "Feature NS-03"
- end
-
- step 'I should not see "Feature NS-03" in merge requests' do
- expect(page).not_to have_content "Feature NS-03"
- end
-
- step 'I should not see "Bug NS-04" in merge requests' do
- expect(page).not_to have_content "Bug NS-04"
- end
-
- step 'I should see that I am subscribed' do
- expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe'
- end
-
- step 'I should see that I am unsubscribed' do
- expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe'
- end
-
- step 'I click button "Unsubscribe"' do
- click_on "Unsubscribe"
- wait_for_requests
- end
-
- step 'I click link "Close"' do
- first(:css, '.close-mr-link').click
- end
-
- step 'I submit new merge request "Wiki Feature"' do
- find('.js-source-branch').click
- find('.dropdown-source-branch .dropdown-content a', text: 'fix').click
-
- find('.js-target-branch').click
- first('.dropdown-target-branch .dropdown-content a', text: 'feature').click
-
- click_button "Compare branches"
- fill_in "merge_request_title", with: "Wiki Feature"
- click_button "Submit merge request"
- end
-
- step 'project "Shop" have "Bug NS-04" open merge request' do
- create(:merge_request,
- title: "Bug NS-04",
- source_project: project,
- target_project: project,
- source_branch: 'fix',
- target_branch: 'merge-test',
- author: project.users.first,
- description: "# Description header"
- )
- end
-
- step 'project "Shop" have "Bug NS-06" open merge request' do
- create(:merge_request,
- title: "Bug NS-06",
- source_project: project,
- target_project: project,
- source_branch: 'fix',
- target_branch: 'feature_conflict',
- author: project.users.first,
- description: "# Description header"
- )
- end
-
- step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do
- create(:merge_request_with_diffs,
- title: "Bug NS-05",
- source_project: project,
- target_project: project,
- author: project.users.first,
- source_branch: 'merge-test')
- end
-
- step 'project "Shop" have "Feature NS-05" merged merge request' do
- create(:merged_merge_request,
- title: "Feature NS-05",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'project "Shop" have "Bug NS-07" open merge request with rebased branch' do
- create(:merge_request, :rebased,
- title: "Bug NS-07",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'project "Shop" have "Bug NS-08" open merge request with diverged branch' do
- create(:merge_request, :diverged,
- title: "Bug NS-08",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'project "Shop" have "Feature NS-03" closed merge request' do
- create(:closed_merge_request,
- title: "Feature NS-03",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'project "Community" has "Bug CO-01" open merge request with diffs inside' do
- project = Project.find_by(name: "Community")
- create(:merge_request_with_diffs,
- title: "Bug CO-01",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do
- merge_request = MergeRequest.find_by(title: 'Bug NS-04')
- create_list(:award_emoji, 2, awardable: merge_request)
- create(:award_emoji, :downvote, awardable: merge_request)
- end
-
- step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do
- awardable = MergeRequest.find_by(title: 'Bug NS-06')
- create(:award_emoji, awardable: awardable)
- create_list(:award_emoji, 2, :downvote, awardable: awardable)
- end
-
- step 'The list should be sorted by "Least popular"' do
- page.within '.mr-list' do
- page.within 'li.merge-request:nth-child(1)' do
- expect(page).to have_content 'Bug NS-06'
- expect(page).to have_content '1 2'
- end
-
- page.within 'li.merge-request:nth-child(2)' do
- expect(page).to have_content 'Bug NS-04'
- expect(page).to have_content '2 1'
- end
-
- page.within 'li.merge-request:nth-child(3)' do
- expect(page).to have_content 'Bug NS-05'
- expect(page).not_to have_content '0 0'
- end
- end
- end
-
- step 'The list should be sorted by "Popularity"' do
- page.within '.mr-list' do
- page.within 'li.merge-request:nth-child(1)' do
- expect(page).to have_content 'Bug NS-04'
- expect(page).to have_content '2 1'
- end
-
- page.within 'li.merge-request:nth-child(2)' do
- expect(page).to have_content 'Bug NS-06'
- expect(page).to have_content '1 2'
- end
-
- page.within 'li.merge-request:nth-child(3)' do
- expect(page).to have_content 'Bug NS-05'
- expect(page).not_to have_content '0 0'
- end
- end
- end
-
- step 'I click on the Changes tab' do
- page.within '.merge-request-tabs' do
- click_link 'Changes'
- end
-
- # Waits for load
- expect(page).to have_css('.tab-content #diffs.active')
- end
-
- step 'I should see the proper Inline and Side-by-side links' do
- expect(page).to have_css('#parallel-diff-btn', count: 1)
- expect(page).to have_css('#inline-diff-btn', count: 1)
- end
-
- step 'I switch to the merge request\'s comments tab' do
- visit project_merge_request_path(project, merge_request)
- end
-
- step 'I click on the commit in the merge request' do
- page.within '.merge-request-tabs' do
- click_link 'Commits'
- end
-
- page.within '.commits' do
- click_link Commit.truncate_sha(sample_commit.id)
- end
- end
-
- step 'I leave a comment on the diff page' do
- init_diff_note
- leave_comment "One comment to rule them all"
- end
-
- step 'I leave a comment on the diff page in commit' do
- click_diff_line(sample_commit.line_code)
- leave_comment "One comment to rule them all"
- end
-
- step 'I leave a comment like "Line is wrong" on diff' do
- init_diff_note
- leave_comment "Line is wrong"
- end
-
- step 'user "John Doe" leaves a comment like "Line is wrong" on diff' do
- mr = MergeRequest.find_by(title: "Bug NS-05")
- create(:diff_note_on_merge_request, project: project,
- noteable: mr,
- author: user_exists("John Doe"),
- note: 'Line is wrong')
- end
-
- step 'I leave a comment like "Line is wrong" on diff in commit' do
- click_diff_line(sample_commit.line_code)
- leave_comment "Line is wrong"
- end
-
- step 'I change the comment "Line is wrong" to "Typo, please fix" on diff' do
- page.within('.diff-file:nth-of-type(5) .note') do
- find('.js-note-edit').click
-
- page.within('.current-note-edit-form', visible: true) do
- fill_in 'note_note', with: 'Typo, please fix'
- click_button 'Save comment'
- end
-
- expect(page).not_to have_button 'Save comment', disabled: true, visible: true
- end
- end
-
- step 'I should not see a diff comment saying "Line is wrong"' do
- page.within('.diff-file:nth-of-type(5) .note') do
- expect(page).not_to have_visible_content 'Line is wrong'
- end
- end
-
- step 'I should see a diff comment saying "Typo, please fix"' do
- page.within('.diff-file:nth-of-type(5) .note') do
- expect(page).to have_visible_content 'Typo, please fix'
- end
- end
-
- step 'I delete the comment "Line is wrong" on diff' do
- page.within('.diff-file:nth-of-type(5) .note') do
- find('.more-actions').click
- find('.more-actions .dropdown-menu li', match: :first)
-
- find('.js-note-delete').click
- end
- end
-
- step 'I click on the Discussion tab' do
- page.within '.merge-request-tabs' do
- find('.notes-tab').trigger('click')
- end
-
- # Waits for load
- expect(page).to have_css('.tab-content #notes.active')
- end
-
- step 'I should not see any discussion' do
- expect(page).not_to have_css('.notes .discussion')
- end
-
- step 'I should see a discussion has started on diff' do
- page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion"
- page.should have_content sample_commit.line_code_path
- page.should have_content "Line is wrong"
- end
- end
-
- step 'I should see a discussion by user "John Doe" has started on diff' do
- # Trigger a refresh of notes
- execute_script("$(document).trigger('visibilitychange');")
- wait_for_requests
- page.within(".notes .discussion") do
- page.should have_content "#{user_exists("John Doe").name} #{user_exists("John Doe").to_reference} started a discussion"
- page.should have_content sample_commit.line_code_path
- page.should have_content "Line is wrong"
- end
- end
-
- step 'I should see a badge of "1" next to the discussion link' do
- expect_discussion_badge_to_have_counter("1")
- wait_for_requests
- end
-
- step 'I should see a badge of "0" next to the discussion link' do
- expect_discussion_badge_to_have_counter("0")
- wait_for_requests
- end
-
- step 'I should see a discussion has started on commit diff' do
- page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
- page.should have_content sample_commit.line_code_path
- page.should have_content "Line is wrong"
- wait_for_requests
- end
- end
-
- step 'I should see a discussion has started on commit' do
- page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
- page.should have_content "One comment to rule them all"
- wait_for_requests
- end
- end
-
- step 'merge request is mergeable' do
- expect(page).to have_button 'Merge'
- end
-
- step 'I modify merge commit message' do
- click_button "Modify commit message"
- fill_in 'Commit message', with: 'wow such merge'
- end
-
- step 'merge request "Bug NS-05" is mergeable' do
- merge_request.mark_as_mergeable
- end
-
- step 'I accept this merge request' do
- page.within '.mr-state-widget' do
- click_button "Merge"
- end
- end
-
- step 'I should see merged request' do
- page.within '.status-box' do
- expect(page).to have_content "Merged"
- wait_for_requests
- end
- end
-
- step 'I click link "Reopen"' do
- first(:css, '.reopen-mr-link').trigger('click')
- end
-
- step 'I should see reopened merge request "Bug NS-04"' do
- page.within '.status-box' do
- expect(page).to have_content "Open"
- end
- wait_for_requests
- end
-
- step 'I click link "Hide inline discussion" of the third file' do
- page.within '.files>div:nth-child(3)' do
- find('.js-toggle-diff-comments').trigger('click')
- end
- end
-
- step 'I click link "Show inline discussion" of the third file' do
- page.within '.files>div:nth-child(3)' do
- find('.js-toggle-diff-comments').trigger('click')
- end
- end
-
- step 'I should not see a comment like "Line is wrong" in the third file' do
- page.within '.files>div:nth-child(3)' do
- expect(page).not_to have_visible_content "Line is wrong"
- end
- end
-
- step 'I should see a comment like "Line is wrong" in the third file' do
- page.within '.files>div:nth-child(3) .note-body > .note-text' do
- expect(page).to have_visible_content "Line is wrong"
- wait_for_requests
- end
- end
-
- step 'I should not see a comment like "Line is wrong here" in the third file' do
- page.within '.files>div:nth-child(3)' do
- expect(page).not_to have_visible_content "Line is wrong here"
- end
- end
-
- step 'I should see a comment like "Line is wrong here" in the third file' do
- page.within '.files>div:nth-child(3) .note-body > .note-text' do
- expect(page).to have_visible_content "Line is wrong here"
- end
- end
-
- step 'I leave a comment like "Line is correct" on line 12 of the second file' do
- init_diff_note_first_file
-
- page.within(".js-discussion-note-form") do
- fill_in "note_note", with: "Line is correct"
- click_button "Comment"
- end
-
- wait_for_requests
-
- page.within ".files>div:nth-child(2) .note-body > .note-text" do
- expect(page).to have_content "Line is correct"
- end
- end
-
- step 'I leave a comment like "Line is wrong" on line 39 of the third file' do
- init_diff_note_second_file
-
- page.within(".js-discussion-note-form") do
- fill_in "note_note", with: "Line is wrong on here"
- click_button "Comment"
- end
-
- wait_for_requests
- end
-
- step 'I should still see a comment like "Line is correct" in the second file' do
- page.within '.files>div:nth-child(2) .note-body > .note-text' do
- expect(page).to have_visible_content "Line is correct"
- end
- end
-
- step 'I unfold diff' do
- expect(page).to have_css('.js-unfold')
-
- first('.js-unfold').click
- end
-
- step 'I should see additional file lines' do
- expect(first('.text-file')).to have_content('.bundle')
- end
-
- step 'I click Side-by-side Diff tab' do
- find('a', text: 'Side-by-side').trigger('click')
-
- # Waits for load
- expect(page).to have_css('.parallel')
- end
-
- step 'I should see comments on the side-by-side diff page' do
- page.within '.files>div:nth-child(2) .parallel .note-body > .note-text' do
- expect(page).to have_visible_content "Line is correct"
- wait_for_requests
- end
- end
-
- step 'I fill in merge request search with "Fe"' do
- fill_in 'issuable_search', with: "Fe"
- page.within '.merge-requests-holder' do
- find('.merge-request')
- end
- end
-
- step 'I click the "Target branch" dropdown' do
- expect(page).to have_content('Target branch')
- first('.target_branch').click
- end
-
- step 'I select a new target branch' do
- select "feature", from: "merge_request_target_branch"
- click_button 'Save'
- end
-
- step 'I should see new target branch changes' do
- expect(page).to have_content 'Request to merge fix into feature'
- expect(page).to have_content 'changed target branch from merge-test to feature'
- wait_for_requests
- end
-
- step 'I click on "Email Patches"' do
- click_link "Email Patches"
- end
-
- step 'I click on "Plain Diff"' do
- click_link "Plain Diff"
- end
-
- step 'I should see a patch diff' do
- expect(page).to have_content('diff --git')
- end
-
- step '"Bug NS-05" has CI status' do
- project = merge_request.source_project
- project.enable_ci
-
- pipeline =
- create(:ci_pipeline,
- project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- head_pipeline_of: merge_request)
-
- create :ci_build, pipeline: pipeline
- end
-
- step 'I should see merge request "Bug NS-05" with CI status' do
- page.within ".mr-list" do
- expect(page).to have_link "Pipeline: pending"
- end
- end
-
- step 'I should see the diverged commits count' do
- page.within ".mr-source-target" do
- expect(page).to have_content /([0-9]+ commits behind)/
- end
-
- wait_for_requests
- end
-
- step 'I should not see the diverged commits count' do
- page.within ".mr-source-target" do
- expect(page).not_to have_content /([0-9]+ commit[s]? behind)/
- end
-
- wait_for_requests
- end
-
- def merge_request
- @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
- end
-
- def init_diff_note
- click_diff_line(sample_commit.line_code)
- end
-
- def leave_comment(message)
- page.within(".js-discussion-note-form", visible: true) do
- fill_in "note_note", with: message
- click_button "Comment"
- end
-
- wait_for_requests
-
- page.within(".notes_holder", visible: true) do
- expect(page).to have_content message
- end
- end
-
- def init_diff_note_first_file
- click_diff_line(sample_compare.changes[0][:line_code])
- end
-
- def init_diff_note_second_file
- click_diff_line(sample_compare.changes[1][:line_code])
- end
-
- def have_visible_content(text)
- have_css("*", text: text, visible: true)
- end
-
- def expect_discussion_badge_to_have_counter(value)
- page.within(".notes-tab .badge") do
- page.should have_content value
- end
- end
-end
diff --git a/features/support/env.rb b/features/support/env.rb
index 608d988755c..5962745d501 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -10,7 +10,7 @@ if ENV['CI']
Knapsack::Adapters::SpinachAdapter.bind
end
-%w(select2_helper test_env repo_helpers wait_for_requests sidekiq).each do |f|
+%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper).each do |f|
require Rails.root.join('spec', 'support', f)
end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 643c8e6fb8e..61a2d688282 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -13,7 +13,7 @@ module API
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository branches' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
use :pagination
@@ -23,13 +23,13 @@ module API
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442
Gitlab::GitalyClient.allow_n_plus_1_calls do
- present paginate(branches), with: Entities::RepoBranch, project: user_project
+ present paginate(branches), with: Entities::Branch, project: user_project
end
end
resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
desc 'Get a single branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -41,7 +41,7 @@ module API
branch = user_project.repository.find_branch(params[:branch])
not_found!('Branch') unless branch
- present branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::Branch, project: user_project
end
end
@@ -50,7 +50,7 @@ module API
# in `gitlab-org/gitlab-ce!5081`. The API interface has not been changed (to maintain compatibility),
# but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`.
desc 'Protect a single branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -80,7 +80,7 @@ module API
end
if protected_branch.valid?
- present branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::Branch, project: user_project
else
render_api_error!(protected_branch.errors.full_messages, 422)
end
@@ -88,7 +88,7 @@ module API
# Note: This API will be deprecated in favor of the protected branches API.
desc 'Unprotect a single branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -101,11 +101,11 @@ module API
protected_branch = user_project.protected_branches.find_by(name: branch.name)
protected_branch&.destroy
- present branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::Branch, project: user_project
end
desc 'Create branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -119,7 +119,7 @@ module API
if result[:status] == :success
present result[:branch],
- with: Entities::RepoBranch,
+ with: Entities::Branch,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 4b8d248f5f7..4af37a2ad1d 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -13,7 +13,7 @@ module API
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository commits' do
- success Entities::RepoCommit
+ success Entities::Commit
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -46,11 +46,11 @@ module API
paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
- present paginate(paginated_commits), with: Entities::RepoCommit
+ present paginate(paginated_commits), with: Entities::Commit
end
desc 'Commit multiple file changes as one commit' do
- success Entities::RepoCommitDetail
+ success Entities::CommitDetail
detail 'This feature was introduced in GitLab 8.13'
end
params do
@@ -72,14 +72,14 @@ module API
if result[:status] == :success
commit_detail = user_project.repository.commit(result[:result])
- present commit_detail, with: Entities::RepoCommitDetail
+ present commit_detail, with: Entities::CommitDetail
else
render_api_error!(result[:message], 400)
end
end
desc 'Get a specific commit of a project' do
- success Entities::RepoCommitDetail
+ success Entities::CommitDetail
failure [[404, 'Commit Not Found']]
end
params do
@@ -90,7 +90,7 @@ module API
not_found! 'Commit' unless commit
- present commit, with: Entities::RepoCommitDetail
+ present commit, with: Entities::CommitDetail
end
desc 'Get the diff for a specific commit of a project' do
@@ -104,7 +104,7 @@ module API
not_found! 'Commit' unless commit
- present commit.raw_diffs.to_a, with: Entities::RepoDiff
+ present commit.raw_diffs.to_a, with: Entities::Diff
end
desc "Get a commit's comments" do
@@ -126,7 +126,7 @@ module API
desc 'Cherry pick commit into a branch' do
detail 'This feature was introduced in GitLab 8.15'
- success Entities::RepoCommit
+ success Entities::Commit
end
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked'
@@ -151,7 +151,7 @@ module API
if result[:status] == :success
branch = user_project.repository.find_branch(params[:branch])
- present user_project.repository.commit(branch.dereferenced_target), with: Entities::RepoCommit
+ present user_project.repository.commit(branch.dereferenced_target), with: Entities::Commit
else
render_api_error!(result[:message], 400)
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 7082f31b5b8..5f0bad14839 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -220,7 +220,7 @@ module API
expose :shared_projects, using: Entities::Project
end
- class RepoCommit < Grape::Entity
+ class Commit < Grape::Entity
expose :id, :short_id, :title, :created_at
expose :parent_ids
expose :safe_message, as: :message
@@ -228,20 +228,20 @@ module API
expose :committer_name, :committer_email, :committed_date
end
- class RepoCommitStats < Grape::Entity
+ class CommitStats < Grape::Entity
expose :additions, :deletions, :total
end
- class RepoCommitDetail < RepoCommit
- expose :stats, using: Entities::RepoCommitStats
+ class CommitDetail < Commit
+ expose :stats, using: Entities::CommitStats
expose :status
expose :last_pipeline, using: 'API::Entities::PipelineBasic'
end
- class RepoBranch < Grape::Entity
+ class Branch < Grape::Entity
expose :name
- expose :commit, using: Entities::RepoCommit do |repo_branch, options|
+ expose :commit, using: Entities::Commit do |repo_branch, options|
options[:project].repository.commit(repo_branch.dereferenced_target)
end
@@ -265,7 +265,7 @@ module API
end
end
- class RepoTreeObject < Grape::Entity
+ class TreeObject < Grape::Entity
expose :id, :name, :type, :path
expose :mode do |obj, options|
@@ -305,7 +305,7 @@ module API
expose :state, :created_at, :updated_at
end
- class RepoDiff < Grape::Entity
+ class Diff < Grape::Entity
expose :old_path, :new_path, :a_mode, :b_mode
expose :new_file?, as: :new_file
expose :renamed_file?, as: :renamed_file
@@ -368,6 +368,7 @@ module API
end
expose :due_date
expose :confidential
+ expose :discussion_locked
expose :web_url do |issue, options|
Gitlab::UrlBuilder.build(issue)
@@ -464,6 +465,7 @@ module API
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
expose :user_notes_count
+ expose :discussion_locked
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
@@ -483,7 +485,7 @@ module API
end
class MergeRequestChanges < MergeRequest
- expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _|
+ expose :diffs, as: :changes, using: Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a
end
end
@@ -494,9 +496,9 @@ module API
end
class MergeRequestDiffFull < MergeRequestDiff
- expose :commits, using: Entities::RepoCommit
+ expose :commits, using: Entities::Commit
- expose :diffs, using: Entities::RepoDiff do |compare, _|
+ expose :diffs, using: Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a
end
end
@@ -592,8 +594,7 @@ module API
expose :target_type
expose :target do |todo, options|
- target = todo.target_type == 'Commit' ? 'RepoCommit' : todo.target_type
- Entities.const_get(target).represent(todo.target, options)
+ Entities.const_get(todo.target_type).represent(todo.target, options)
end
expose :target_url do |todo, options|
@@ -729,15 +730,15 @@ module API
end
class Compare < Grape::Entity
- expose :commit, using: Entities::RepoCommit do |compare, options|
- Commit.decorate(compare.commits, nil).last
+ expose :commit, using: Entities::Commit do |compare, options|
+ ::Commit.decorate(compare.commits, nil).last
end
- expose :commits, using: Entities::RepoCommit do |compare, options|
- Commit.decorate(compare.commits, nil)
+ expose :commits, using: Entities::Commit do |compare, options|
+ ::Commit.decorate(compare.commits, nil)
end
- expose :diffs, using: Entities::RepoDiff do |compare, options|
+ expose :diffs, using: Entities::Diff do |compare, options|
compare.diffs(limits: false).to_a
end
@@ -773,10 +774,10 @@ module API
expose :description
end
- class RepoTag < Grape::Entity
+ class Tag < Grape::Entity
expose :name, :message
- expose :commit, using: Entities::RepoCommit do |repo_tag, options|
+ expose :commit, using: Entities::Commit do |repo_tag, options|
options[:project].repository.commit(repo_tag.dereferenced_target)
end
@@ -827,7 +828,7 @@ module API
expose :created_at, :started_at, :finished_at
expose :user, with: User
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
- expose :commit, with: RepoCommit
+ expose :commit, with: Commit
expose :runner, with: Runner
expose :pipeline, with: PipelineBasic
end
@@ -880,7 +881,7 @@ module API
expose :deployable, using: Entities::Job
end
- class RepoLicense < Grape::Entity
+ class License < Grape::Entity
expose :key, :name, :nickname
expose :featured, as: :popular
expose :url, as: :html_url
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 4964a76bef6..a87297a604c 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -287,7 +287,7 @@ module API
if sentry_enabled? && report_exception?(exception)
define_params_for_grape_middleware
sentry_context
- Raven.capture_exception(exception)
+ Raven.capture_exception(exception, extra: params)
end
# lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 1729df2aad0..0df41dcc903 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -48,6 +48,7 @@ module API
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
+ optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
end
params :issue_params do
@@ -193,7 +194,7 @@ module API
desc: 'Date time when the issue was updated. Available only for admins and project owners.'
optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
use :issue_params
- at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id,
+ at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id, :discussion_locked,
:labels, :created_at, :due_date, :confidential, :state_event
end
put ':id/issues/:issue_iid' do
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 8aa1e0216ee..be843ec8251 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -183,13 +183,13 @@ module API
end
desc 'Get the commits of a merge request' do
- success Entities::RepoCommit
+ success Entities::Commit
end
get ':id/merge_requests/:merge_request_iid/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
commits = ::Kaminari.paginate_array(merge_request.commits)
- present paginate(commits), with: Entities::RepoCommit
+ present paginate(commits), with: Entities::Commit
end
desc 'Show the merge request changes' do
@@ -214,12 +214,14 @@ module API
:remove_source_branch,
:state_event,
:target_branch,
- :title
+ :title,
+ :discussion_locked
]
optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen],
desc: 'Status of the merge request'
+ optional :discussion_locked, type: Boolean, desc: 'Whether the MR discussion is locked'
use :optional_params
at_least_one_of(*at_least_one_of_ce)
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index d6e7203adaf..0b9ab4eeb05 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -78,6 +78,8 @@ module API
}
if can?(current_user, noteable_read_ability_name(noteable), noteable)
+ authorize! :create_note, noteable
+
if params[:created_at] && (current_user.admin? || user_project.owner == current_user)
opts[:created_at] = params[:created_at]
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 2255fb1b70d..ceee3226732 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -35,7 +35,7 @@ module API
end
desc 'Get a project repository tree' do
- success Entities::RepoTreeObject
+ success Entities::TreeObject
end
params do
optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -52,7 +52,7 @@ module API
tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
entries = ::Kaminari.paginate_array(tree.sorted_entries)
- present paginate(entries), with: Entities::RepoTreeObject
+ present paginate(entries), with: Entities::TreeObject
end
desc 'Get raw blob contents from the repository'
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 912415e3a7f..0d394a7b441 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -11,18 +11,18 @@ module API
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository tags' do
- success Entities::RepoTag
+ success Entities::Tag
end
params do
use :pagination
end
get ':id/repository/tags' do
tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse)
- present paginate(tags), with: Entities::RepoTag, project: user_project
+ present paginate(tags), with: Entities::Tag, project: user_project
end
desc 'Get a single repository tag' do
- success Entities::RepoTag
+ success Entities::Tag
end
params do
requires :tag_name, type: String, desc: 'The name of the tag'
@@ -31,11 +31,11 @@ module API
tag = user_project.repository.find_tag(params[:tag_name])
not_found!('Tag') unless tag
- present tag, with: Entities::RepoTag, project: user_project
+ present tag, with: Entities::Tag, project: user_project
end
desc 'Create a new repository tag' do
- success Entities::RepoTag
+ success Entities::Tag
end
params do
requires :tag_name, type: String, desc: 'The name of the tag'
@@ -51,7 +51,7 @@ module API
if result[:status] == :success
present result[:tag],
- with: Entities::RepoTag,
+ with: Entities::Tag,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index f70bc0622b7..6550b331fb8 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -49,7 +49,7 @@ module API
desc 'Get the list of the available license template' do
detail 'This feature was introduced in GitLab 8.7.'
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
@@ -60,12 +60,12 @@ module API
featured: declared(params)[:popular].present? ? true : nil
}
licences = ::Kaminari.paginate_array(Licensee::License.all(options))
- present paginate(licences), with: Entities::RepoLicense
+ present paginate(licences), with: Entities::License
end
desc 'Get the text for a specific license' do
detail 'This feature was introduced in GitLab 8.7.'
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
requires :name, type: String, desc: 'The name of the template'
@@ -75,7 +75,7 @@ module API
template = parsed_license_template
- present template, with: ::API::Entities::RepoLicense
+ present template, with: ::API::Entities::License
end
GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
diff --git a/lib/api/users.rb b/lib/api/users.rb
index d07dc302717..b6f97a1eac2 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -331,7 +331,6 @@ module API
email = Emails::CreateService.new(current_user, declared_params(include_missing: false).merge(user: user)).execute
if email.errors.blank?
- NotificationService.new.new_email(email)
present email, with: Entities::Email
else
render_validation_error!(email)
@@ -369,10 +368,8 @@ module API
not_found!('Email') unless email
destroy_conditionally!(email) do |email|
- Emails::DestroyService.new(current_user, user: user, email: email.email).execute
+ Emails::DestroyService.new(current_user, user: user).execute(email)
end
-
- user.update_secondary_emails!
end
desc 'Delete a user. Available only for admins.' do
@@ -677,7 +674,6 @@ module API
email = Emails::CreateService.new(current_user, declared_params.merge(user: current_user)).execute
if email.errors.blank?
- NotificationService.new.new_email(email)
present email, with: Entities::Email
else
render_validation_error!(email)
@@ -693,10 +689,8 @@ module API
not_found!('Email') unless email
destroy_conditionally!(email) do |email|
- Emails::DestroyService.new(current_user, user: current_user, email: email.email).execute
+ Emails::DestroyService.new(current_user, user: current_user).execute(email)
end
-
- current_user.update_secondary_emails!
end
desc 'Get a list of user activities'
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
index 81b13249892..69cd12de72c 100644
--- a/lib/api/v3/branches.rb
+++ b/lib/api/v3/branches.rb
@@ -11,12 +11,12 @@ module API
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository branches' do
- success ::API::Entities::RepoBranch
+ success ::API::Entities::Branch
end
get ":id/repository/branches" do
branches = user_project.repository.branches.sort_by(&:name)
- present branches, with: ::API::Entities::RepoBranch, project: user_project
+ present branches, with: ::API::Entities::Branch, project: user_project
end
desc 'Delete a branch'
@@ -47,7 +47,7 @@ module API
end
desc 'Create branch' do
- success ::API::Entities::RepoBranch
+ success ::API::Entities::Branch
end
params do
requires :branch_name, type: String, desc: 'The name of the branch'
@@ -60,7 +60,7 @@ module API
if result[:status] == :success
present result[:branch],
- with: ::API::Entities::RepoBranch,
+ with: ::API::Entities::Branch,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
index 5936f4700aa..345cb7e7c11 100644
--- a/lib/api/v3/commits.rb
+++ b/lib/api/v3/commits.rb
@@ -13,7 +13,7 @@ module API
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository commits' do
- success ::API::Entities::RepoCommit
+ success ::API::Entities::Commit
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -34,11 +34,11 @@ module API
after: params[:since],
before: params[:until])
- present commits, with: ::API::Entities::RepoCommit
+ present commits, with: ::API::Entities::Commit
end
desc 'Commit multiple file changes as one commit' do
- success ::API::Entities::RepoCommitDetail
+ success ::API::Entities::CommitDetail
detail 'This feature was introduced in GitLab 8.13'
end
params do
@@ -59,14 +59,14 @@ module API
if result[:status] == :success
commit_detail = user_project.repository.commits(result[:result], limit: 1).first
- present commit_detail, with: ::API::Entities::RepoCommitDetail
+ present commit_detail, with: ::API::Entities::CommitDetail
else
render_api_error!(result[:message], 400)
end
end
desc 'Get a specific commit of a project' do
- success ::API::Entities::RepoCommitDetail
+ success ::API::Entities::CommitDetail
failure [[404, 'Not Found']]
end
params do
@@ -77,7 +77,7 @@ module API
not_found! "Commit" unless commit
- present commit, with: ::API::Entities::RepoCommitDetail
+ present commit, with: ::API::Entities::CommitDetail
end
desc 'Get the diff for a specific commit of a project' do
@@ -113,7 +113,7 @@ module API
desc 'Cherry pick commit into a branch' do
detail 'This feature was introduced in GitLab 8.15'
- success ::API::Entities::RepoCommit
+ success ::API::Entities::Commit
end
params do
requires :sha, type: String, desc: 'A commit sha to be cherry picked'
@@ -138,7 +138,7 @@ module API
if result[:status] == :success
branch = user_project.repository.find_branch(params[:branch])
- present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::RepoCommit
+ present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::Commit
else
render_api_error!(result[:message], 400)
end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index c928ce5265b..afdd7b83998 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -220,7 +220,7 @@ module API
expose :created_at, :started_at, :finished_at
expose :user, with: ::API::Entities::User
expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? }
- expose :commit, with: ::API::Entities::RepoCommit
+ expose :commit, with: ::API::Entities::Commit
expose :runner, with: ::API::Entities::Runner
expose :pipeline, with: ::API::Entities::PipelineBasic
end
@@ -237,7 +237,7 @@ module API
end
class MergeRequestChanges < MergeRequest
- expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _|
+ expose :diffs, as: :changes, using: ::API::Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a
end
end
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index b6b7254ae29..1d6d823f32b 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -135,12 +135,12 @@ module API
end
desc 'Get the commits of a merge request' do
- success ::API::Entities::RepoCommit
+ success ::API::Entities::Commit
end
get "#{path}/commits" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
- present merge_request.commits, with: ::API::Entities::RepoCommit
+ present merge_request.commits, with: ::API::Entities::Commit
end
desc 'Show the merge request changes' do
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
index 0eaa0de2eef..41a7c6b83ae 100644
--- a/lib/api/v3/repositories.rb
+++ b/lib/api/v3/repositories.rb
@@ -19,7 +19,7 @@ module API
end
desc 'Get a project repository tree' do
- success ::API::Entities::RepoTreeObject
+ success ::API::Entities::TreeObject
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -35,7 +35,7 @@ module API
tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
- present tree.sorted_entries, with: ::API::Entities::RepoTreeObject
+ present tree.sorted_entries, with: ::API::Entities::TreeObject
end
desc 'Get a raw file contents'
diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb
index 7e5875cd030..6e37d31d153 100644
--- a/lib/api/v3/tags.rb
+++ b/lib/api/v3/tags.rb
@@ -8,11 +8,11 @@ module API
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository tags' do
- success ::API::Entities::RepoTag
+ success ::API::Entities::Tag
end
get ":id/repository/tags" do
tags = user_project.repository.tags.sort_by(&:name).reverse
- present tags, with: ::API::Entities::RepoTag, project: user_project
+ present tags, with: ::API::Entities::Tag, project: user_project
end
desc 'Delete a repository tag'
diff --git a/lib/api/v3/templates.rb b/lib/api/v3/templates.rb
index 2a2fb59045c..7298203df10 100644
--- a/lib/api/v3/templates.rb
+++ b/lib/api/v3/templates.rb
@@ -52,7 +52,7 @@ module API
detailed_desc = 'This feature was introduced in GitLab 8.7.'
detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
@@ -61,7 +61,7 @@ module API
options = {
featured: declared(params)[:popular].present? ? true : nil
}
- present Licensee::License.all(options), with: ::API::Entities::RepoLicense
+ present Licensee::License.all(options), with: ::API::Entities::License
end
end
@@ -70,7 +70,7 @@ module API
detailed_desc = 'This feature was introduced in GitLab 8.7.'
detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
requires :name, type: String, desc: 'The name of the template'
@@ -80,7 +80,7 @@ module API
template = parsed_license_template
- present template, with: ::API::Entities::RepoLicense
+ present template, with: ::API::Entities::License
end
end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index ceca9296851..5f91884a878 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -40,7 +40,7 @@ module Banzai
return cacheless_render_field(object, field)
end
- object.refresh_markdown_cache!(do_update: update_object?(object)) unless object.cached_html_up_to_date?(field)
+ object.refresh_markdown_cache! unless object.cached_html_up_to_date?(field)
object.cached_html_for(field)
end
@@ -162,10 +162,5 @@ module Banzai
return unless cache_key
Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend
end
-
- # GitLab EE needs to disable updates on GET requests in Geo
- def self.update_object?(object)
- true
- end
end
end
diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb
index bfcec241489..7cfa82a9a9f 100644
--- a/lib/declarative_policy/rule.rb
+++ b/lib/declarative_policy/rule.rb
@@ -206,11 +206,13 @@ module DeclarativePolicy
end
def cached_pass?(context)
- passes = @rules.map { |r| r.cached_pass?(context) }
- return false if passes.any? { |p| p == false }
- return true if passes.all? { |p| p == true }
+ @rules.each do |rule|
+ pass = rule.cached_pass?(context)
- nil
+ return pass if pass.nil? || pass == false
+ end
+
+ true
end
def repr
@@ -245,11 +247,13 @@ module DeclarativePolicy
end
def cached_pass?(context)
- passes = @rules.map { |r| r.cached_pass?(context) }
- return true if passes.any? { |p| p == true }
- return false if passes.all? { |p| p == false }
+ @rules.each do |rule|
+ pass = rule.cached_pass?(context)
- nil
+ return pass if pass.nil? || pass == true
+ end
+
+ false
end
def score(context)
diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb
index 56afd1f1392..45ff2ef9ced 100644
--- a/lib/declarative_policy/runner.rb
+++ b/lib/declarative_policy/runner.rb
@@ -107,7 +107,7 @@ module DeclarativePolicy
end
# This is the core spot where all those `#score` methods matter.
- # It is critcal for performance to run steps in the correct order,
+ # It is critical for performance to run steps in the correct order,
# so that we don't compute expensive conditions (potentially n times
# if we're called on, say, a large list of users).
#
@@ -139,30 +139,39 @@ module DeclarativePolicy
return
end
- steps = Set.new(@steps)
- remaining_enablers = steps.count { |s| s.enable? }
+ remaining_steps = Set.new(@steps)
+ remaining_enablers, remaining_preventers = remaining_steps.partition(&:enable?).map { |s| Set.new(s) }
loop do
- return if steps.empty?
+ if @state.enabled?
+ # Once we set this, we never need to unset it, because a single
+ # prevent will stop this from being enabled
+ remaining_steps = remaining_preventers
+ else
+ # if the permission hasn't yet been enabled and we only have
+ # prevent steps left, we short-circuit the state here
+ @state.prevent! if remaining_enablers.empty?
+ end
- # if the permission hasn't yet been enabled and we only have
- # prevent steps left, we short-circuit the state here
- @state.prevent! if !@state.enabled? && remaining_enablers == 0
+ return if remaining_steps.empty?
lowest_score = Float::INFINITY
next_step = nil
- steps.each do |step|
+ remaining_steps.each do |step|
score = step.score
+
if score < lowest_score
next_step = step
lowest_score = score
end
- end
- steps.delete(next_step)
+ break if lowest_score.zero?
+ end
- remaining_enablers -= 1 if next_step.enable?
+ [remaining_steps, remaining_enablers, remaining_preventers].each do |set|
+ set.delete(next_step)
+ end
yield next_step, lowest_score
end
diff --git a/lib/github/import.rb b/lib/github/import.rb
index c0cd8382875..55f8387f27a 100644
--- a/lib/github/import.rb
+++ b/lib/github/import.rb
@@ -9,7 +9,7 @@ module Github
include Gitlab::ShellAdapter
attr_reader :project, :repository, :repo, :repo_url, :wiki_url,
- :options, :errors, :cached, :verbose
+ :options, :errors, :cached, :verbose, :last_fetched_at
def initialize(project, options = {})
@project = project
@@ -21,12 +21,13 @@ module Github
@verbose = options.fetch(:verbose, false)
@cached = Hash.new { |hash, key| hash[key] = Hash.new }
@errors = []
+ @last_fetched_at = nil
end
# rubocop: disable Rails/Output
def execute
puts 'Fetching repository...'.color(:aqua) if verbose
- fetch_repository
+ setup_and_fetch_repository
puts 'Fetching labels...'.color(:aqua) if verbose
fetch_labels
puts 'Fetching milestones...'.color(:aqua) if verbose
@@ -42,7 +43,7 @@ module Github
puts 'Expiring repository cache...'.color(:aqua) if verbose
expire_repository_cache
- true
+ errors.empty?
rescue Github::RepositoryFetchError
expire_repository_cache
false
@@ -52,18 +53,24 @@ module Github
private
- def fetch_repository
+ def setup_and_fetch_repository
begin
project.ensure_repository
project.repository.add_remote('github', repo_url)
- project.repository.set_remote_as_mirror('github')
- project.repository.fetch_remote('github', forced: true)
+ project.repository.set_import_remote_as_mirror('github')
+ project.repository.add_remote_fetch_config('github', '+refs/pull/*/head:refs/merge-requests/*/head')
+ fetch_remote(forced: true)
rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e
error(:project, repo_url, e.message)
raise Github::RepositoryFetchError
end
end
+ def fetch_remote(forced: false)
+ @last_fetched_at = Time.now
+ project.repository.fetch_remote('github', forced: forced)
+ end
+
def fetch_wiki_repository
return if project.wiki.repository_exists?
@@ -92,7 +99,7 @@ module Github
label.color = representation.color
end
- cached[:label_ids][label.title] = label.id
+ cached[:label_ids][representation.title] = label.id
rescue => e
error(:label, representation.url, e.message)
end
@@ -143,7 +150,9 @@ module Github
next unless merge_request.new_record? && pull_request.valid?
begin
- pull_request.restore_branches!
+ # If the PR has been created/updated after we last fetched the
+ # remote, we fetch again to get the up-to-date refs.
+ fetch_remote if pull_request.updated_at > last_fetched_at
author_id = user_id(pull_request.author, project.creator_id)
description = format_description(pull_request.description, pull_request.author)
@@ -152,6 +161,7 @@ module Github
iid: pull_request.iid,
title: pull_request.title,
description: description,
+ ref_fetched: true,
source_project: pull_request.source_project,
source_branch: pull_request.source_branch_name,
source_branch_sha: pull_request.source_branch_sha,
@@ -173,8 +183,6 @@ module Github
fetch_comments(merge_request, :review_comment, review_comments_url, LegacyDiffNote)
rescue => e
error(:pull_request, pull_request.url, e.message)
- ensure
- pull_request.remove_restored_branches!
end
end
@@ -203,11 +211,11 @@ module Github
# for both features, like manipulating assignees, labels
# and milestones, are provided within the Issues API.
if representation.pull_request?
- return unless representation.has_labels? || representation.has_comments?
+ return unless representation.labels? || representation.comments?
merge_request = MergeRequest.find_by!(target_project_id: project.id, iid: representation.iid)
- if representation.has_labels?
+ if representation.labels?
merge_request.update_attribute(:label_ids, label_ids(representation.labels))
end
@@ -222,14 +230,16 @@ module Github
issue.title = representation.title
issue.description = format_description(representation.description, representation.author)
issue.state = representation.state
- issue.label_ids = label_ids(representation.labels)
issue.milestone_id = milestone_id(representation.milestone)
issue.author_id = author_id
- issue.assignee_ids = [user_id(representation.assignee)]
issue.created_at = representation.created_at
issue.updated_at = representation.updated_at
issue.save!(validate: false)
+ issue.update(
+ label_ids: label_ids(representation.labels),
+ assignee_ids: assignee_ids(representation.assignees))
+
fetch_comments_conditionally(issue, representation)
end
rescue => e
@@ -238,7 +248,7 @@ module Github
end
def fetch_comments_conditionally(issuable, representation)
- if representation.has_comments?
+ if representation.comments?
comments_url = "/repos/#{repo}/issues/#{issuable.iid}/comments"
fetch_comments(issuable, :comment, comments_url)
end
@@ -302,7 +312,11 @@ module Github
end
def label_ids(labels)
- labels.map { |attrs| cached[:label_ids][attrs.fetch('name')] }.compact
+ labels.map { |label| cached[:label_ids][label.title] }.compact
+ end
+
+ def assignee_ids(assignees)
+ assignees.map { |assignee| user_id(assignee) }.compact
end
def milestone_id(milestone)
diff --git a/lib/github/representation/branch.rb b/lib/github/representation/branch.rb
index 823e8e9a9c4..0087a3d3c4f 100644
--- a/lib/github/representation/branch.rb
+++ b/lib/github/representation/branch.rb
@@ -7,10 +7,14 @@ module Github
raw.dig('user', 'login') || 'unknown'
end
+ def repo?
+ raw['repo'].present?
+ end
+
def repo
- return @repo if defined?(@repo)
+ return unless repo?
- @repo = Github::Representation::Repo.new(raw['repo']) if raw['repo'].present?
+ @repo ||= Github::Representation::Repo.new(raw['repo'])
end
def ref
@@ -25,10 +29,6 @@ module Github
Commit.truncate_sha(sha)
end
- def exists?
- @exists ||= branch_exists? && commit_exists?
- end
-
def valid?
sha.present? && ref.present?
end
@@ -47,14 +47,6 @@ module Github
private
- def branch_exists?
- repository.branch_exists?(ref)
- end
-
- def commit_exists?
- repository.branch_names_contains(sha).include?(ref)
- end
-
def repository
@repository ||= options.fetch(:repository)
end
diff --git a/lib/github/representation/issuable.rb b/lib/github/representation/issuable.rb
index 9713b82615d..768ba3b993c 100644
--- a/lib/github/representation/issuable.rb
+++ b/lib/github/representation/issuable.rb
@@ -23,14 +23,14 @@ module Github
@author ||= Github::Representation::User.new(raw['user'], options)
end
- def assignee
- return unless assigned?
-
- @assignee ||= Github::Representation::User.new(raw['assignee'], options)
+ def labels?
+ raw['labels'].any?
end
- def assigned?
- raw['assignee'].present?
+ def labels
+ @labels ||= Array(raw['labels']).map do |label|
+ Github::Representation::Label.new(label, options)
+ end
end
end
end
diff --git a/lib/github/representation/issue.rb b/lib/github/representation/issue.rb
index df3540a6e6c..4f1a02cb90f 100644
--- a/lib/github/representation/issue.rb
+++ b/lib/github/representation/issue.rb
@@ -1,25 +1,27 @@
module Github
module Representation
class Issue < Representation::Issuable
- def labels
- raw['labels']
- end
-
def state
raw['state'] == 'closed' ? 'closed' : 'opened'
end
- def has_comments?
+ def comments?
raw['comments'] > 0
end
- def has_labels?
- labels.count > 0
- end
-
def pull_request?
raw['pull_request'].present?
end
+
+ def assigned?
+ raw['assignees'].present?
+ end
+
+ def assignees
+ @assignees ||= Array(raw['assignees']).map do |user|
+ Github::Representation::User.new(user, options)
+ end
+ end
end
end
end
diff --git a/lib/github/representation/pull_request.rb b/lib/github/representation/pull_request.rb
index 55461097e8a..0171179bb0f 100644
--- a/lib/github/representation/pull_request.rb
+++ b/lib/github/representation/pull_request.rb
@@ -1,26 +1,17 @@
module Github
module Representation
class PullRequest < Representation::Issuable
- delegate :user, :repo, :ref, :sha, to: :source_branch, prefix: true
- delegate :user, :exists?, :repo, :ref, :sha, :short_sha, to: :target_branch, prefix: true
+ delegate :sha, to: :source_branch, prefix: true
+ delegate :sha, to: :target_branch, prefix: true
def source_project
project
end
def source_branch_name
- @source_branch_name ||=
- if cross_project? || !source_branch_exists?
- source_branch_name_prefixed
- else
- source_branch_ref
- end
- end
-
- def source_branch_exists?
- return @source_branch_exists if defined?(@source_branch_exists)
-
- @source_branch_exists = !cross_project? && source_branch.exists?
+ # Mimic the "user:branch" displayed in the MR widget,
+ # i.e. "Request to merge rymai:add-external-mounts into master"
+ cross_project? ? "#{source_branch.user}:#{source_branch.ref}" : source_branch.ref
end
def target_project
@@ -28,11 +19,7 @@ module Github
end
def target_branch_name
- @target_branch_name ||= target_branch_exists? ? target_branch_ref : target_branch_name_prefixed
- end
-
- def target_branch_exists?
- @target_branch_exists ||= target_branch.exists?
+ target_branch.ref
end
def state
@@ -50,16 +37,14 @@ module Github
source_branch.valid? && target_branch.valid?
end
- def restore_branches!
- restore_source_branch!
- restore_target_branch!
+ def assigned?
+ raw['assignee'].present?
end
- def remove_restored_branches!
- return if opened?
+ def assignee
+ return unless assigned?
- remove_source_branch!
- remove_target_branch!
+ @assignee ||= Github::Representation::User.new(raw['assignee'], options)
end
private
@@ -72,48 +57,14 @@ module Github
@source_branch ||= Representation::Branch.new(raw['head'], repository: project.repository)
end
- def source_branch_name_prefixed
- "gh-#{target_branch_short_sha}/#{iid}/#{source_branch_user}/#{source_branch_ref}"
- end
-
def target_branch
@target_branch ||= Representation::Branch.new(raw['base'], repository: project.repository)
end
- def target_branch_name_prefixed
- "gl-#{target_branch_short_sha}/#{iid}/#{target_branch_user}/#{target_branch_ref}"
- end
-
def cross_project?
- return true if source_branch_repo.nil?
-
- source_branch_repo.id != target_branch_repo.id
- end
-
- def restore_source_branch!
- return if source_branch_exists?
-
- source_branch.restore!(source_branch_name)
- end
-
- def restore_target_branch!
- return if target_branch_exists?
-
- target_branch.restore!(target_branch_name)
- end
-
- def remove_source_branch!
- # We should remove the source/target branches only if they were
- # restored. Otherwise, we'll remove branches like 'master' that
- # target_branch_exists? returns true. In other words, we need
- # to clean up only the restored branches that (source|target)_branch_exists?
- # returns false for the first time it has been called, because of
- # this that is important to memoize these values.
- source_branch.remove!(source_branch_name) unless source_branch_exists?
- end
+ return true unless source_branch.repo?
- def remove_target_branch!
- target_branch.remove!(target_branch_name) unless target_branch_exists?
+ source_branch.repo.id != target_branch.repo.id
end
end
end
diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
new file mode 100644
index 00000000000..c88eb9783ed
--- /dev/null
+++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
@@ -0,0 +1,65 @@
+module Gitlab
+ module BackgroundMigration
+ class CreateForkNetworkMembershipsRange
+ RESCHEDULE_DELAY = 15
+
+ class ForkedProjectLink < ActiveRecord::Base
+ self.table_name = 'forked_project_links'
+ end
+
+ def perform(start_id, end_id)
+ log("Creating memberships for forks: #{start_id} - #{end_id}")
+
+ ActiveRecord::Base.connection.execute <<~INSERT_MEMBERS
+ INSERT INTO fork_network_members (fork_network_id, project_id, forked_from_project_id)
+
+ SELECT fork_network_members.fork_network_id,
+ forked_project_links.forked_to_project_id,
+ forked_project_links.forked_from_project_id
+
+ FROM forked_project_links
+
+ INNER JOIN fork_network_members
+ ON forked_project_links.forked_from_project_id = fork_network_members.project_id
+
+ WHERE forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ AND NOT EXISTS (
+ SELECT true
+ FROM fork_network_members existing_members
+ WHERE existing_members.project_id = forked_project_links.forked_to_project_id
+ )
+ INSERT_MEMBERS
+
+ if missing_members?(start_id, end_id)
+ BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [start_id, end_id])
+ end
+ end
+
+ def missing_members?(start_id, end_id)
+ count_sql = <<~MISSING_MEMBERS
+ SELECT COUNT(*)
+
+ FROM forked_project_links
+
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM fork_network_members
+ WHERE fork_network_members.project_id = forked_project_links.forked_to_project_id
+ )
+ AND EXISTS (
+ SELECT true
+ FROM projects
+ WHERE forked_project_links.forked_from_project_id = projects.id
+ )
+ AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ MISSING_MEMBERS
+
+ ForkNetworkMember.count_by_sql(count_sql) > 0
+ end
+
+ def log(message)
+ Rails.logger.info("#{self.class.name} - #{message}")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb b/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
new file mode 100644
index 00000000000..e94719db72e
--- /dev/null
+++ b/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
@@ -0,0 +1,53 @@
+class Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys
+ class GpgKey < ActiveRecord::Base
+ self.table_name = 'gpg_keys'
+
+ include EachBatch
+ include ShaAttribute
+
+ sha_attribute :primary_keyid
+ sha_attribute :fingerprint
+
+ has_many :subkeys, class_name: 'GpgKeySubkey'
+ end
+
+ class GpgKeySubkey < ActiveRecord::Base
+ self.table_name = 'gpg_key_subkeys'
+
+ include ShaAttribute
+
+ sha_attribute :keyid
+ sha_attribute :fingerprint
+ end
+
+ def perform(gpg_key_id)
+ gpg_key = GpgKey.find_by(id: gpg_key_id)
+
+ return if gpg_key.nil?
+ return if gpg_key.subkeys.any?
+
+ create_subkeys(gpg_key)
+ update_signatures(gpg_key)
+ end
+
+ private
+
+ def create_subkeys(gpg_key)
+ gpg_subkeys = Gitlab::Gpg.subkeys_from_key(gpg_key.key)
+
+ gpg_subkeys[gpg_key.primary_keyid.upcase]&.each do |subkey_data|
+ gpg_key.subkeys.build(keyid: subkey_data[:keyid], fingerprint: subkey_data[:fingerprint])
+ end
+
+ # Improve latency by doing all INSERTs in a single call
+ GpgKey.transaction do
+ gpg_key.save!
+ end
+ end
+
+ def update_signatures(gpg_key)
+ return unless gpg_key.subkeys.exists?
+
+ InvalidGpgSignatureUpdateWorker.perform_async(gpg_key.id)
+ end
+end
diff --git a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
new file mode 100644
index 00000000000..bc53e6d7f94
--- /dev/null
+++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
@@ -0,0 +1,313 @@
+module Gitlab
+ module BackgroundMigration
+ class NormalizeLdapExternUidsRange
+ class Identity < ActiveRecord::Base
+ self.table_name = 'identities'
+ end
+
+ # Copied this class to make this migration resilient to future code changes.
+ # And if the normalize behavior is changed in the future, it must be
+ # accompanied by another migration.
+ module Gitlab
+ module LDAP
+ class DN
+ FormatError = Class.new(StandardError)
+ MalformedError = Class.new(FormatError)
+ UnsupportedError = Class.new(FormatError)
+
+ def self.normalize_value(given_value)
+ dummy_dn = "placeholder=#{given_value}"
+ normalized_dn = new(*dummy_dn).to_normalized_s
+ normalized_dn.sub(/\Aplaceholder=/, '')
+ end
+
+ ##
+ # Initialize a DN, escaping as required. Pass in attributes in name/value
+ # pairs. If there is a left over argument, it will be appended to the dn
+ # without escaping (useful for a base string).
+ #
+ # Most uses of this class will be to escape a DN, rather than to parse it,
+ # so storing the dn as an escaped String and parsing parts as required
+ # with a state machine seems sensible.
+ def initialize(*args)
+ if args.length > 1
+ initialize_array(args)
+ else
+ initialize_string(args[0])
+ end
+ end
+
+ ##
+ # Parse a DN into key value pairs using ASN from
+ # http://tools.ietf.org/html/rfc2253 section 3.
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def each_pair
+ state = :key
+ key = StringIO.new
+ value = StringIO.new
+ hex_buffer = ""
+
+ @dn.each_char.with_index do |char, dn_index|
+ case state
+ when :key then
+ case char
+ when 'a'..'z', 'A'..'Z' then
+ state = :key_normal
+ key << char
+ when '0'..'9' then
+ state = :key_oid
+ key << char
+ when ' ' then state = :key
+ else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
+ end
+ when :key_normal then
+ case char
+ when '=' then state = :value
+ when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
+ end
+ when :key_oid then
+ case char
+ when '=' then state = :value
+ when '0'..'9', '.', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
+ end
+ when :value then
+ case char
+ when '\\' then state = :value_normal_escape
+ when '"' then state = :value_quoted
+ when ' ' then state = :value
+ when '#' then
+ state = :value_hexstring
+ value << char
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal then
+ case char
+ when '\\' then state = :value_normal_escape
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
+ else value << char
+ end
+ when :value_normal_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal_escape_hex
+ hex_buffer = char
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
+ end
+ when :value_quoted then
+ case char
+ when '\\' then state = :value_quoted_escape
+ when '"' then state = :value_end
+ else value << char
+ end
+ when :value_quoted_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted_escape_hex
+ hex_buffer = char
+ else
+ state = :value_quoted
+ value << char
+ end
+ when :value_quoted_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
+ end
+ when :value_hexstring then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring_hex
+ value << char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_hexstring_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring
+ value << char
+ else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_end then
+ case char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
+ end
+ else raise "Fell out of state machine"
+ end
+ end
+
+ # Last pair
+ raise(MalformedError, 'DN string ended unexpectedly') unless
+ [:value, :value_normal, :value_hexstring, :value_end].include? state
+
+ yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
+ end
+
+ def rstrip_except_escaped(str, dn_index)
+ str_ends_with_whitespace = str.match(/\s\z/)
+
+ if str_ends_with_whitespace
+ dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
+
+ if dn_part_ends_with_escaped_whitespace
+ dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
+ num_chars_to_remove = dn_part_rwhitespace.length - 1
+ str = str[0, str.length - num_chars_to_remove]
+ else
+ str.rstrip!
+ end
+ end
+
+ str
+ end
+
+ ##
+ # Returns the DN as an array in the form expected by the constructor.
+ def to_a
+ a = []
+ self.each_pair { |key, value| a << key << value } unless @dn.empty?
+ a
+ end
+
+ ##
+ # Return the DN as an escaped string.
+ def to_s
+ @dn
+ end
+
+ ##
+ # Return the DN as an escaped and normalized string.
+ def to_normalized_s
+ self.class.new(*to_a).to_s.downcase
+ end
+
+ # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
+ # for DN values. All of the following must be escaped in any normal string
+ # using a single backslash ('\') as escape. The space character is left
+ # out here because in a "normalized" string, spaces should only be escaped
+ # if necessary (i.e. leading or trailing space).
+ NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
+
+ # The following must be represented as escaped hex
+ HEX_ESCAPES = {
+ "\n" => '\0a',
+ "\r" => '\0d'
+ }.freeze
+
+ # Compiled character class regexp using the keys from the above hash, and
+ # checking for a space or # at the start, or space at the end, of the
+ # string.
+ ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
+ NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ HEX_ESCAPE_RE = Regexp.new("([" +
+ HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ ##
+ # Escape a string for use in a DN value
+ def self.escape(string)
+ escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
+ escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
+ end
+
+ private
+
+ def initialize_array(args)
+ buffer = StringIO.new
+
+ args.each_with_index do |arg, index|
+ if index.even? # key
+ buffer << "," if index > 0
+ buffer << arg
+ else # value
+ buffer << "="
+ buffer << self.class.escape(arg)
+ end
+ end
+
+ @dn = buffer.string
+ end
+
+ def initialize_string(arg)
+ @dn = arg.to_s
+ end
+
+ ##
+ # Proxy all other requests to the string object, because a DN is mainly
+ # used within the library as a string
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @dn.send(method, *args, &block)
+ end
+
+ ##
+ # Redefined to be consistent with redefined `method_missing` behavior
+ def respond_to?(sym, include_private = false)
+ @dn.respond_to?(sym, include_private)
+ end
+ end
+ end
+ end
+
+ def perform(start_id, end_id)
+ return unless migrate?
+
+ ldap_identities = Identity.where("provider like 'ldap%'").where(id: start_id..end_id)
+ ldap_identities.each do |identity|
+ begin
+ identity.extern_uid = Gitlab::LDAP::DN.new(identity.extern_uid).to_normalized_s
+ unless identity.save
+ Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\". Skipping."
+ end
+ rescue Gitlab::LDAP::DN::FormatError => e
+ Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\" due to \"#{e.message}\". Skipping."
+ end
+ end
+ end
+
+ def migrate?
+ Identity.table_exists?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/populate_fork_networks_range.rb b/lib/gitlab/background_migration/populate_fork_networks_range.rb
new file mode 100644
index 00000000000..2ef3a207dd3
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_fork_networks_range.rb
@@ -0,0 +1,59 @@
+module Gitlab
+ module BackgroundMigration
+ class PopulateForkNetworksRange
+ def perform(start_id, end_id)
+ log("Creating fork networks for forked project links: #{start_id} - #{end_id}")
+
+ ActiveRecord::Base.connection.execute <<~INSERT_NETWORKS
+ INSERT INTO fork_networks (root_project_id)
+ SELECT DISTINCT forked_project_links.forked_from_project_id
+
+ FROM forked_project_links
+
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM forked_project_links inner_links
+ WHERE inner_links.forked_to_project_id = forked_project_links.forked_from_project_id
+ )
+ AND NOT EXISTS (
+ SELECT true
+ FROM fork_networks
+ WHERE forked_project_links.forked_from_project_id = fork_networks.root_project_id
+ )
+ AND EXISTS (
+ SELECT true
+ FROM projects
+ WHERE projects.id = forked_project_links.forked_from_project_id
+ )
+ AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ INSERT_NETWORKS
+
+ log("Creating memberships for root projects: #{start_id} - #{end_id}")
+
+ ActiveRecord::Base.connection.execute <<~INSERT_ROOT
+ INSERT INTO fork_network_members (fork_network_id, project_id)
+ SELECT DISTINCT fork_networks.id, fork_networks.root_project_id
+
+ FROM fork_networks
+
+ INNER JOIN forked_project_links
+ ON forked_project_links.forked_from_project_id = fork_networks.root_project_id
+
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM fork_network_members
+ WHERE fork_network_members.project_id = fork_networks.root_project_id
+ )
+ AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ INSERT_ROOT
+
+ delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY
+ BackgroundMigrationWorker.perform_in(delay, "CreateForkNetworkMembershipsRange", [start_id, end_id])
+ end
+
+ def log(message)
+ Rails.logger.info("#{self.class.name} - #{message}")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index 088adbdd267..72b75791bbb 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -155,7 +155,7 @@ module Gitlab
stream.each_line do |line|
s = StringScanner.new(line)
until s.eos?
- if s.scan(/section_((?:start)|(?:end)):(\d+):([^\r]+)\r\033\[0K/)
+ if s.scan(Gitlab::Regex.build_trace_section_regex)
handle_section(s)
elsif s.scan(/\e([@-_])(.*?)([@-~])/)
handle_sequence(s)
diff --git a/lib/gitlab/ci/pipeline/chain/validate/config.rb b/lib/gitlab/ci/pipeline/chain/validate/config.rb
index 489bcd79655..075504bcce5 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/config.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/config.rb
@@ -13,7 +13,7 @@ module Gitlab
end
if @command.save_incompleted && @pipeline.has_yaml_errors?
- @pipeline.drop
+ @pipeline.drop!(:config_error)
end
return error(@pipeline.yaml_errors)
diff --git a/lib/gitlab/ci/stage/seed.rb b/lib/gitlab/ci/stage/seed.rb
index e19aae35a81..bc97aa63b02 100644
--- a/lib/gitlab/ci/stage/seed.rb
+++ b/lib/gitlab/ci/stage/seed.rb
@@ -3,7 +3,9 @@ module Gitlab
module Stage
class Seed
attr_reader :pipeline
+
delegate :project, to: :pipeline
+ delegate :size, to: :@jobs
def initialize(pipeline, stage, jobs)
@pipeline = pipeline
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index 5b835bb669a..baf55b1fa07 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -27,6 +27,12 @@ module Gitlab
end
end
+ def extract_sections
+ read do |stream|
+ stream.extract_sections
+ end
+ end
+
def set(data)
write do |stream|
data = job.hide_secrets(data)
diff --git a/lib/gitlab/ci/trace/section_parser.rb b/lib/gitlab/ci/trace/section_parser.rb
new file mode 100644
index 00000000000..9bb0166c9e3
--- /dev/null
+++ b/lib/gitlab/ci/trace/section_parser.rb
@@ -0,0 +1,97 @@
+module Gitlab
+ module Ci
+ class Trace
+ class SectionParser
+ def initialize(lines)
+ @lines = lines
+ end
+
+ def parse!
+ @markers = {}
+
+ @lines.each do |line, pos|
+ parse_line(line, pos)
+ end
+ end
+
+ def sections
+ sanitize_markers.map do |name, markers|
+ start_, end_ = markers
+
+ {
+ name: name,
+ byte_start: start_[:marker],
+ byte_end: end_[:marker],
+ date_start: start_[:timestamp],
+ date_end: end_[:timestamp]
+ }
+ end
+ end
+
+ private
+
+ def parse_line(line, line_start_position)
+ s = StringScanner.new(line)
+ until s.eos?
+ find_next_marker(s) do |scanner|
+ marker_begins_at = line_start_position + scanner.pointer
+
+ if scanner.scan(Gitlab::Regex.build_trace_section_regex)
+ marker_ends_at = line_start_position + scanner.pointer
+ handle_line(scanner[1], scanner[2].to_i, scanner[3], marker_begins_at, marker_ends_at)
+ true
+ else
+ false
+ end
+ end
+ end
+ end
+
+ def sanitize_markers
+ @markers.select do |_, markers|
+ markers.size == 2 && markers[0][:action] == :start && markers[1][:action] == :end
+ end
+ end
+
+ def handle_line(action, time, name, marker_start, marker_end)
+ action = action.to_sym
+ timestamp = Time.at(time).utc
+ marker = if action == :start
+ marker_end
+ else
+ marker_start
+ end
+
+ @markers[name] ||= []
+ @markers[name] << {
+ name: name,
+ action: action,
+ timestamp: timestamp,
+ marker: marker
+ }
+ end
+
+ def beginning_of_section_regex
+ @beginning_of_section_regex ||= /section_/.freeze
+ end
+
+ def find_next_marker(s)
+ beginning_of_section_len = 8
+ maybe_marker = s.exist?(beginning_of_section_regex)
+
+ if maybe_marker.nil?
+ s.terminate
+ else
+ # repositioning at the beginning of the match
+ s.pos += maybe_marker - beginning_of_section_len
+ if block_given?
+ good_marker = yield(s)
+ # if not a good marker: Consuming the matched beginning_of_section_regex
+ s.pos += beginning_of_section_len unless good_marker
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index ab3408f48d6..d52194f688b 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -90,8 +90,25 @@ module Gitlab
# so we just silently ignore error for now
end
+ def extract_sections
+ return [] unless valid?
+
+ lines = to_enum(:each_line_with_pos)
+ parser = SectionParser.new(lines)
+
+ parser.parse!
+ parser.sections
+ end
+
private
+ def each_line_with_pos
+ stream.seek(0, IO::SEEK_SET)
+ stream.each_line do |line|
+ yield [line, stream.pos - line.bytesize]
+ end
+ end
+
def read_last_lines(limit)
to_enum(:reverse_line).first(limit).reverse.join
end
diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb
index 243c1f1394d..7e7aaeeaa17 100644
--- a/lib/gitlab/closing_issue_extractor.rb
+++ b/lib/gitlab/closing_issue_extractor.rb
@@ -23,7 +23,8 @@ module Gitlab
@extractor.analyze(closing_statements.join(" "))
@extractor.issues.reject do |issue|
- @extractor.project.forked_from?(issue.project) # Don't extract issues on original project
+ # Don't extract issues from the project this project was forked from
+ @extractor.project.forked_from?(issue.project)
end
end
end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 31a46a738c3..c169c8fe135 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -86,7 +86,7 @@ module Gitlab
user_name: user.name,
user_username: user.username,
user_email: user.email,
- user_avatar: user.avatar_url,
+ user_avatar: user.avatar_url(only_path: false),
project_id: project.id,
project: project.hook_attrs,
commits: commit_attrs,
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index a6ec75da385..357f16936c6 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -29,6 +29,15 @@ module Gitlab
adapter_name.casecmp('postgresql').zero?
end
+ # Overridden in EE
+ def self.read_only?
+ false
+ end
+
+ def self.read_write?
+ !self.read_only?
+ end
+
def self.version
database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index fcac85ff892..599c3c5deab 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -27,16 +27,23 @@ module Gitlab
@fallback_diff_refs = fallback_diff_refs
end
- def position(line)
+ def position(position_marker, position_type: :text)
return unless diff_refs
- Position.new(
+ data = {
+ diff_refs: diff_refs,
+ position_type: position_type.to_s,
old_path: old_path,
- new_path: new_path,
- old_line: line.old_line,
- new_line: line.new_line,
- diff_refs: diff_refs
- )
+ new_path: new_path
+ }
+
+ if position_type == :text
+ data.merge!(text_position_properties(position_marker))
+ else
+ data.merge!(image_position_properties(position_marker))
+ end
+
+ Position.new(data)
end
def line_code(line)
@@ -228,6 +235,14 @@ module Gitlab
private
+ def text_position_properties(line)
+ { old_line: line.old_line, new_line: line.new_line }
+ end
+
+ def image_position_properties(image_point)
+ image_point.to_h
+ end
+
def blobs_changed?
old_blob && new_blob && old_blob.id != new_blob.id
end
diff --git a/lib/gitlab/diff/formatters/base_formatter.rb b/lib/gitlab/diff/formatters/base_formatter.rb
new file mode 100644
index 00000000000..5e923b9e602
--- /dev/null
+++ b/lib/gitlab/diff/formatters/base_formatter.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module Diff
+ module Formatters
+ class BaseFormatter
+ attr_reader :old_path
+ attr_reader :new_path
+ attr_reader :base_sha
+ attr_reader :start_sha
+ attr_reader :head_sha
+ attr_reader :position_type
+
+ def initialize(attrs)
+ if diff_file = attrs[:diff_file]
+ attrs[:diff_refs] = diff_file.diff_refs
+ attrs[:old_path] = diff_file.old_path
+ attrs[:new_path] = diff_file.new_path
+ end
+
+ if diff_refs = attrs[:diff_refs]
+ attrs[:base_sha] = diff_refs.base_sha
+ attrs[:start_sha] = diff_refs.start_sha
+ attrs[:head_sha] = diff_refs.head_sha
+ end
+
+ @old_path = attrs[:old_path]
+ @new_path = attrs[:new_path]
+ @base_sha = attrs[:base_sha]
+ @start_sha = attrs[:start_sha]
+ @head_sha = attrs[:head_sha]
+ end
+
+ def key
+ [base_sha, start_sha, head_sha, Digest::SHA1.hexdigest(old_path || ""), Digest::SHA1.hexdigest(new_path || "")]
+ end
+
+ def to_h
+ {
+ base_sha: base_sha,
+ start_sha: start_sha,
+ head_sha: head_sha,
+ old_path: old_path,
+ new_path: new_path,
+ position_type: position_type
+ }
+ end
+
+ def position_type
+ raise NotImplementedError
+ end
+
+ def ==(other)
+ raise NotImplementedError
+ end
+
+ def complete?
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/formatters/image_formatter.rb b/lib/gitlab/diff/formatters/image_formatter.rb
new file mode 100644
index 00000000000..ccd0d309972
--- /dev/null
+++ b/lib/gitlab/diff/formatters/image_formatter.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Diff
+ module Formatters
+ class ImageFormatter < BaseFormatter
+ attr_reader :width
+ attr_reader :height
+ attr_reader :x
+ attr_reader :y
+
+ def initialize(attrs)
+ @x = attrs[:x]
+ @y = attrs[:y]
+ @width = attrs[:width]
+ @height = attrs[:height]
+
+ super(attrs)
+ end
+
+ def key
+ @key ||= super.push(x, y)
+ end
+
+ def complete?
+ x && y && width && height
+ end
+
+ def to_h
+ super.merge(width: width, height: height, x: x, y: y)
+ end
+
+ def position_type
+ "image"
+ end
+
+ def ==(other)
+ other.is_a?(self.class) &&
+ x == other.x &&
+ y == other.y
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/formatters/text_formatter.rb b/lib/gitlab/diff/formatters/text_formatter.rb
new file mode 100644
index 00000000000..01c7e9f51ab
--- /dev/null
+++ b/lib/gitlab/diff/formatters/text_formatter.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Diff
+ module Formatters
+ class TextFormatter < BaseFormatter
+ attr_reader :old_line
+ attr_reader :new_line
+
+ def initialize(attrs)
+ @old_line = attrs[:old_line]
+ @new_line = attrs[:new_line]
+
+ super(attrs)
+ end
+
+ def key
+ @key ||= super.push(old_line, new_line)
+ end
+
+ def complete?
+ old_line || new_line
+ end
+
+ def to_h
+ super.merge(old_line: old_line, new_line: new_line)
+ end
+
+ def line_age
+ if old_line && new_line
+ nil
+ elsif new_line
+ 'new'
+ else
+ 'old'
+ end
+ end
+
+ def position_type
+ "text"
+ end
+
+ def ==(other)
+ other.is_a?(self.class) &&
+ new_line == other.new_line &&
+ old_line == other.old_line
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/image_point.rb b/lib/gitlab/diff/image_point.rb
new file mode 100644
index 00000000000..65332dfd239
--- /dev/null
+++ b/lib/gitlab/diff/image_point.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Diff
+ class ImagePoint
+ attr_reader :width, :height, :x, :y
+
+ def initialize(width, height, x, y)
+ @width = width
+ @height = height
+ @x = x
+ @y = y
+ end
+
+ def to_h
+ {
+ width: width,
+ height: height,
+ x: x,
+ y: y
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index b8db3adef0a..bd0a9502a5e 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -1,37 +1,25 @@
-# Defines a specific location, identified by paths and line numbers,
+# Defines a specific location, identified by paths line numbers and image coordinates,
# within a specific diff, identified by start, head and base commit ids.
module Gitlab
module Diff
class Position
- attr_reader :old_path
- attr_reader :new_path
- attr_reader :old_line
- attr_reader :new_line
- attr_reader :base_sha
- attr_reader :start_sha
- attr_reader :head_sha
-
+ attr_accessor :formatter
+
+ delegate :old_path,
+ :new_path,
+ :base_sha,
+ :start_sha,
+ :head_sha,
+ :old_line,
+ :new_line,
+ :position_type, to: :formatter
+
+ # A position can belong to a text line or to an image coordinate
+ # it depends of the position_type argument.
+ # Text position will have: new_line and old_line
+ # Image position will have: width, height, x, y
def initialize(attrs = {})
- if diff_file = attrs[:diff_file]
- attrs[:diff_refs] = diff_file.diff_refs
- attrs[:old_path] = diff_file.old_path
- attrs[:new_path] = diff_file.new_path
- end
-
- if diff_refs = attrs[:diff_refs]
- attrs[:base_sha] = diff_refs.base_sha
- attrs[:start_sha] = diff_refs.start_sha
- attrs[:head_sha] = diff_refs.head_sha
- end
-
- @old_path = attrs[:old_path]
- @new_path = attrs[:new_path]
- @base_sha = attrs[:base_sha]
- @start_sha = attrs[:start_sha]
- @head_sha = attrs[:head_sha]
-
- @old_line = attrs[:old_line]
- @new_line = attrs[:new_line]
+ @formatter = get_formatter_class(attrs[:position_type]).new(attrs)
end
# `Gitlab::Diff::Position` objects are stored as serialized attributes in
@@ -46,7 +34,11 @@ module Gitlab
end
def encode_with(coder)
- coder['attributes'] = self.to_h
+ coder['attributes'] = formatter.to_h
+ end
+
+ def key
+ formatter.key
end
def ==(other)
@@ -54,20 +46,11 @@ module Gitlab
other.diff_refs == diff_refs &&
other.old_path == old_path &&
other.new_path == new_path &&
- other.old_line == old_line &&
- other.new_line == new_line
+ other.formatter == formatter
end
def to_h
- {
- old_path: old_path,
- new_path: new_path,
- old_line: old_line,
- new_line: new_line,
- base_sha: base_sha,
- start_sha: start_sha,
- head_sha: head_sha
- }
+ formatter.to_h
end
def inspect
@@ -75,23 +58,15 @@ module Gitlab
end
def complete?
- file_path.present? &&
- (old_line || new_line) &&
- diff_refs.complete?
+ file_path.present? && formatter.complete? && diff_refs.complete?
end
def to_json(opts = nil)
- JSON.generate(self.to_h, opts)
+ JSON.generate(formatter.to_h, opts)
end
def type
- if old_line && new_line
- nil
- elsif new_line
- 'new'
- else
- 'old'
- end
+ formatter.line_age
end
def unchanged?
@@ -150,6 +125,17 @@ module Gitlab
diff_refs.compare_in(repository.project).diffs(paths: paths, expanded: true).diff_files.first
end
+
+ def get_formatter_class(type)
+ type ||= "text"
+
+ case type
+ when 'image'
+ Gitlab::Diff::Formatters::ImageFormatter
+ else
+ Gitlab::Diff::Formatters::TextFormatter
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index c5a8ea12245..c4c60d1dfee 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -2,7 +2,7 @@
module Gitlab
# Checks if a set of migrations requires downtime or not.
class EeCompatCheck
- CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
+ DEFAULT_CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
@@ -20,7 +20,7 @@ module Gitlab
attr_reader :ee_repo_dir, :patches_dir, :ce_repo, :ce_branch, :ee_branch_found
attr_reader :failed_files
- def initialize(branch:, ce_repo: CE_REPO)
+ def initialize(branch:, ce_repo: DEFAULT_CE_REPO)
@ee_repo_dir = CHECK_DIR.join('ee-repo')
@patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch
@@ -132,7 +132,7 @@ module Gitlab
def check_patch(patch_path)
step("Checking out master", %w[git checkout master])
step("Resetting to latest master", %w[git reset --hard origin/master])
- step("Fetching CE/#{ce_branch}", %W[git fetch #{CE_REPO} #{ce_branch}])
+ step("Fetching CE/#{ce_branch}", %W[git fetch #{ce_repo} #{ce_branch}])
step(
"Checking if #{patch_path} applies cleanly to EE/master",
# Don't use --check here because it can result in a 0-exit status even
diff --git a/lib/gitlab/gcp/model.rb b/lib/gitlab/gcp/model.rb
new file mode 100644
index 00000000000..195391f0e3c
--- /dev/null
+++ b/lib/gitlab/gcp/model.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Gcp
+ module Model
+ def table_name_prefix
+ "gcp_"
+ end
+
+ def model_name
+ @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 89b654253cb..0f059bef808 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -990,7 +990,7 @@ module Gitlab
tmp_ref = fetch_ref(
start_repository,
source_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
- target_ref: "refs/tmp/#{SecureRandom.hex}/head"
+ target_ref: "refs/tmp/#{SecureRandom.hex}"
)
yield commit(sha)
@@ -1112,6 +1112,8 @@ module Gitlab
raise NoRepository.new(e)
rescue GRPC::BadStatus => e
raise CommandError.new(e)
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError.new(e)
end
private
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index db67ede9d9e..42b59c106e2 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -17,7 +17,8 @@ module Gitlab
command_not_allowed: "The command you're trying to execute is not allowed.",
upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
- readonly: 'The repository is temporarily read-only. Please try again later.'
+ read_only: 'The repository is temporarily read-only. Please try again later.',
+ cannot_push_to_read_only: "You can't push code to a read-only GitLab instance."
}.freeze
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
@@ -161,7 +162,11 @@ module Gitlab
def check_push_access!(changes)
if project.repository_read_only?
- raise UnauthorizedError, ERROR_MESSAGES[:readonly]
+ raise UnauthorizedError, ERROR_MESSAGES[:read_only]
+ end
+
+ if Gitlab::Database.read_only?
+ raise UnauthorizedError, ERROR_MESSAGES[:cannot_push_to_read_only]
end
if deploy_key
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index 1fe5155c093..98f1f45b338 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -1,6 +1,7 @@
module Gitlab
class GitAccessWiki < GitAccess
ERROR_MESSAGES = {
+ read_only: "You can't push code to a read-only GitLab instance.",
write_to_wiki: "You are not allowed to write to this project's wiki."
}.freeze
@@ -17,6 +18,10 @@ module Gitlab
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end
+ if Gitlab::Database.read_only?
+ raise UnauthorizedError, ERROR_MESSAGES[:read_only]
+ end
+
true
end
end
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index a3c6b21a6a1..2e3e4fc3f1f 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -11,7 +11,7 @@ module Gitlab
return false if ref_name.start_with?('refs/remotes/')
Gitlab::Utils.system_silent(
- %W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name}))
+ %W(#{Gitlab.config.git.bin_path} check-ref-format --branch #{ref_name}))
end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 87b300dcf7e..cf36106e23d 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -28,6 +28,7 @@ module Gitlab
SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
MAXIMUM_GITALY_CALLS = 30
+ CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze
MUTEX = Mutex.new
private_constant :MUTEX
@@ -79,7 +80,16 @@ module Gitlab
def self.request_metadata(storage)
encoded_token = Base64.strict_encode64(token(storage).to_s)
- { metadata: { 'authorization' => "Bearer #{encoded_token}" } }
+ metadata = {
+ 'authorization' => "Bearer #{encoded_token}",
+ 'client_name' => CLIENT_NAME
+ }
+
+ feature_stack = Thread.current[:gitaly_feature_stack]
+ feature = feature_stack && feature_stack[0]
+ metadata['call_site'] = feature.to_s if feature
+
+ { metadata: metadata }
end
def self.token(storage)
@@ -137,7 +147,14 @@ module Gitlab
Gitlab::Metrics.measure(metric_name) do
# Some migrate calls wrap other migrate calls
allow_n_plus_1_calls do
- yield is_enabled
+ feature_stack = Thread.current[:gitaly_feature_stack] ||= []
+ feature_stack.unshift(feature)
+ begin
+ yield is_enabled
+ ensure
+ feature_stack.shift
+ Thread.current[:gitaly_feature_stack] = nil if feature_stack.empty?
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/namespace_service.rb b/lib/gitlab/gitaly_client/namespace_service.rb
new file mode 100644
index 00000000000..bd7c345ac01
--- /dev/null
+++ b/lib/gitlab/gitaly_client/namespace_service.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module GitalyClient
+ class NamespaceService
+ def initialize(storage)
+ @storage = storage
+ end
+
+ def exists?(name)
+ request = Gitaly::NamespaceExistsRequest.new(storage_name: @storage, name: name)
+
+ gitaly_client_call(:namespace_exists, request).exists
+ end
+
+ def add(name)
+ request = Gitaly::AddNamespaceRequest.new(storage_name: @storage, name: name)
+
+ gitaly_client_call(:add_namespace, request)
+ end
+
+ def remove(name)
+ request = Gitaly::RemoveNamespaceRequest.new(storage_name: @storage, name: name)
+
+ gitaly_client_call(:remove_namespace, request)
+ end
+
+ def rename(from, to)
+ request = Gitaly::RenameNamespaceRequest.new(storage_name: @storage, from: from, to: to)
+
+ gitaly_client_call(:rename_namespace, request)
+ end
+
+ private
+
+ def gitaly_client_call(type, request)
+ GitalyClient.call(@storage, :namespace_service, type, request)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb
index 0d5039ddf5f..413872d7e08 100644
--- a/lib/gitlab/gpg.rb
+++ b/lib/gitlab/gpg.rb
@@ -34,6 +34,21 @@ module Gitlab
end
end
+ def subkeys_from_key(key)
+ using_tmp_keychain do
+ fingerprints = CurrentKeyChain.fingerprints_from_key(key)
+ raw_keys = GPGME::Key.find(:public, fingerprints)
+
+ raw_keys.each_with_object({}) do |raw_key, grouped_subkeys|
+ primary_subkey_id = raw_key.primary_subkey.keyid
+
+ grouped_subkeys[primary_subkey_id] = raw_key.subkeys[1..-1].map do |s|
+ { keyid: s.keyid, fingerprint: s.fingerprint }
+ end
+ end
+ end
+ end
+
def user_infos_from_key(key)
using_tmp_keychain do
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 86bd9f5b125..0f4ba6f83fc 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -43,7 +43,9 @@ module Gitlab
# key belonging to the keyid.
# This way we can add the key to the temporary keychain and extract
# the proper signature.
- gpg_key = GpgKey.find_by(primary_keyid: verified_signature.fingerprint)
+ # NOTE: the invoked method is #fingerprint but it's only returning
+ # 16 characters (the format used by keyid) instead of 40.
+ gpg_key = find_gpg_key(verified_signature.fingerprint)
if gpg_key
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
@@ -74,7 +76,7 @@ module Gitlab
commit_sha: @commit.sha,
project: @commit.project,
gpg_key: gpg_key,
- gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint,
+ gpg_key_primary_keyid: gpg_key&.keyid || verified_signature.fingerprint,
gpg_key_user_name: user_infos[:name],
gpg_key_user_email: user_infos[:email],
verification_status: verification_status
@@ -98,6 +100,10 @@ module Gitlab
def user_infos(gpg_key)
gpg_key&.verified_user_infos&.first || gpg_key&.user_infos&.first || {}
end
+
+ def find_gpg_key(keyid)
+ GpgKey.find_by(primary_keyid: keyid) || GpgKeySubkey.find_by(keyid: keyid)
+ end
end
end
end
diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
index e085eab26c9..1991911ef6a 100644
--- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
+++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
@@ -9,8 +9,8 @@ module Gitlab
GpgSignature
.select(:id, :commit_sha, :project_id)
.where('gpg_key_id IS NULL OR verification_status <> ?', GpgSignature.verification_statuses[:verified])
- .where(gpg_key_primary_keyid: @gpg_key.primary_keyid)
- .find_each { |sig| sig.gpg_commit.update_signature!(sig) }
+ .where(gpg_key_primary_keyid: @gpg_key.keyids)
+ .find_each { |sig| sig.gpg_commit&.update_signature!(sig) }
end
end
end
diff --git a/lib/gitlab/hook_data/issuable_builder.rb b/lib/gitlab/hook_data/issuable_builder.rb
new file mode 100644
index 00000000000..4febb0ab430
--- /dev/null
+++ b/lib/gitlab/hook_data/issuable_builder.rb
@@ -0,0 +1,56 @@
+module Gitlab
+ module HookData
+ class IssuableBuilder
+ CHANGES_KEYS = %i[previous current].freeze
+
+ attr_accessor :issuable
+
+ def initialize(issuable)
+ @issuable = issuable
+ end
+
+ def build(user: nil, changes: {})
+ hook_data = {
+ object_kind: issuable.class.name.underscore,
+ user: user.hook_attrs,
+ project: issuable.project.hook_attrs,
+ object_attributes: issuable.hook_attrs,
+ labels: issuable.labels.map(&:hook_attrs),
+ changes: final_changes(changes.slice(*safe_keys)),
+ # DEPRECATED
+ repository: issuable.project.hook_attrs.slice(:name, :url, :description, :homepage)
+ }
+
+ if issuable.is_a?(Issue)
+ hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any?
+ else
+ hook_data[:assignee] = issuable.assignee.hook_attrs if issuable.assignee
+ end
+
+ hook_data
+ end
+
+ def safe_keys
+ issuable_builder::SAFE_HOOK_ATTRIBUTES + issuable_builder::SAFE_HOOK_RELATIONS
+ end
+
+ private
+
+ def issuable_builder
+ case issuable
+ when Issue
+ Gitlab::HookData::IssueBuilder
+ when MergeRequest
+ Gitlab::HookData::MergeRequestBuilder
+ end
+ end
+
+ def final_changes(changes_hash)
+ changes_hash.reduce({}) do |hash, (key, changes_array)|
+ hash[key] = Hash[CHANGES_KEYS.zip(changes_array)]
+ hash
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb
new file mode 100644
index 00000000000..de9cab80a02
--- /dev/null
+++ b/lib/gitlab/hook_data/issue_builder.rb
@@ -0,0 +1,55 @@
+module Gitlab
+ module HookData
+ class IssueBuilder
+ SAFE_HOOK_ATTRIBUTES = %i[
+ assignee_id
+ author_id
+ branch_name
+ closed_at
+ confidential
+ created_at
+ deleted_at
+ description
+ due_date
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ milestone_id
+ moved_to_id
+ project_id
+ relative_position
+ state
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].freeze
+
+ SAFE_HOOK_RELATIONS = %i[
+ assignees
+ labels
+ ].freeze
+
+ attr_accessor :issue
+
+ def initialize(issue)
+ @issue = issue
+ end
+
+ def build
+ attrs = {
+ url: Gitlab::UrlBuilder.build(issue),
+ total_time_spent: issue.total_time_spent,
+ human_total_time_spent: issue.human_total_time_spent,
+ human_time_estimate: issue.human_time_estimate,
+ assignee_ids: issue.assignee_ids,
+ assignee_id: issue.assignee_ids.first # This key is deprecated
+ }
+
+ issue.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES)
+ .merge!(attrs)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb
new file mode 100644
index 00000000000..eaef19c9d04
--- /dev/null
+++ b/lib/gitlab/hook_data/merge_request_builder.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ module HookData
+ class MergeRequestBuilder
+ SAFE_HOOK_ATTRIBUTES = %i[
+ assignee_id
+ author_id
+ created_at
+ deleted_at
+ description
+ head_pipeline_id
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ merge_commit_sha
+ merge_error
+ merge_params
+ merge_status
+ merge_user_id
+ merge_when_pipeline_succeeds
+ milestone_id
+ ref_fetched
+ source_branch
+ source_project_id
+ state
+ target_branch
+ target_project_id
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].freeze
+
+ SAFE_HOOK_RELATIONS = %i[
+ assignee
+ labels
+ ].freeze
+
+ attr_accessor :merge_request
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ def build
+ attrs = {
+ url: Gitlab::UrlBuilder.build(merge_request),
+ source: merge_request.source_project.try(:hook_attrs),
+ target: merge_request.target_project.hook_attrs,
+ last_commit: merge_request.diff_head_commit&.hook_attrs,
+ work_in_progress: merge_request.work_in_progress?,
+ total_time_spent: merge_request.total_time_spent,
+ human_total_time_spent: merge_request.human_total_time_spent,
+ human_time_estimate: merge_request.human_time_estimate
+ }
+
+ merge_request.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES)
+ .merge!(attrs)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 2171c6c7bbb..dec8b4c5acd 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -53,6 +53,7 @@ project_tree:
- :auto_devops
- :triggers
- :pipeline_schedules
+ - :cluster
- :services
- :hooks
- protected_branches:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 380b336395d..a76cf1addc0 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -8,6 +8,8 @@ module Gitlab
triggers: 'Ci::Trigger',
pipeline_schedules: 'Ci::PipelineSchedule',
builds: 'Ci::Build',
+ cluster: 'Gcp::Cluster',
+ clusters: 'Gcp::Cluster',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
index 4fbc5fa5262..3123da17fd9 100644
--- a/lib/gitlab/ldap/auth_hash.rb
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -3,6 +3,10 @@
module Gitlab
module LDAP
class AuthHash < Gitlab::OAuth::AuthHash
+ def uid
+ Gitlab::LDAP::Person.normalize_dn(super)
+ end
+
private
def get_info(key)
diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb
new file mode 100644
index 00000000000..d6142dc6549
--- /dev/null
+++ b/lib/gitlab/ldap/dn.rb
@@ -0,0 +1,301 @@
+# -*- ruby encoding: utf-8 -*-
+
+# Based on the `ruby-net-ldap` gem's `Net::LDAP::DN`
+#
+# For our purposes, this class is used to normalize DNs in order to allow proper
+# comparison.
+#
+# E.g. DNs should be compared case-insensitively (in basically all LDAP
+# implementations or setups), therefore we downcase every DN.
+
+##
+# Objects of this class represent an LDAP DN ("Distinguished Name"). A DN
+# ("Distinguished Name") is a unique identifier for an entry within an LDAP
+# directory. It is made up of a number of other attributes strung together,
+# to identify the entry in the tree.
+#
+# Each attribute that makes up a DN needs to have its value escaped so that
+# the DN is valid. This class helps take care of that.
+#
+# A fully escaped DN needs to be unescaped when analysing its contents. This
+# class also helps take care of that.
+module Gitlab
+ module LDAP
+ class DN
+ FormatError = Class.new(StandardError)
+ MalformedError = Class.new(FormatError)
+ UnsupportedError = Class.new(FormatError)
+
+ def self.normalize_value(given_value)
+ dummy_dn = "placeholder=#{given_value}"
+ normalized_dn = new(*dummy_dn).to_normalized_s
+ normalized_dn.sub(/\Aplaceholder=/, '')
+ end
+
+ ##
+ # Initialize a DN, escaping as required. Pass in attributes in name/value
+ # pairs. If there is a left over argument, it will be appended to the dn
+ # without escaping (useful for a base string).
+ #
+ # Most uses of this class will be to escape a DN, rather than to parse it,
+ # so storing the dn as an escaped String and parsing parts as required
+ # with a state machine seems sensible.
+ def initialize(*args)
+ if args.length > 1
+ initialize_array(args)
+ else
+ initialize_string(args[0])
+ end
+ end
+
+ ##
+ # Parse a DN into key value pairs using ASN from
+ # http://tools.ietf.org/html/rfc2253 section 3.
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def each_pair
+ state = :key
+ key = StringIO.new
+ value = StringIO.new
+ hex_buffer = ""
+
+ @dn.each_char.with_index do |char, dn_index|
+ case state
+ when :key then
+ case char
+ when 'a'..'z', 'A'..'Z' then
+ state = :key_normal
+ key << char
+ when '0'..'9' then
+ state = :key_oid
+ key << char
+ when ' ' then state = :key
+ else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
+ end
+ when :key_normal then
+ case char
+ when '=' then state = :value
+ when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
+ end
+ when :key_oid then
+ case char
+ when '=' then state = :value
+ when '0'..'9', '.', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
+ end
+ when :value then
+ case char
+ when '\\' then state = :value_normal_escape
+ when '"' then state = :value_quoted
+ when ' ' then state = :value
+ when '#' then
+ state = :value_hexstring
+ value << char
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal then
+ case char
+ when '\\' then state = :value_normal_escape
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
+ else value << char
+ end
+ when :value_normal_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal_escape_hex
+ hex_buffer = char
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
+ end
+ when :value_quoted then
+ case char
+ when '\\' then state = :value_quoted_escape
+ when '"' then state = :value_end
+ else value << char
+ end
+ when :value_quoted_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted_escape_hex
+ hex_buffer = char
+ else
+ state = :value_quoted
+ value << char
+ end
+ when :value_quoted_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
+ end
+ when :value_hexstring then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring_hex
+ value << char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_hexstring_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring
+ value << char
+ else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_end then
+ case char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
+ end
+ else raise "Fell out of state machine"
+ end
+ end
+
+ # Last pair
+ raise(MalformedError, 'DN string ended unexpectedly') unless
+ [:value, :value_normal, :value_hexstring, :value_end].include? state
+
+ yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
+ end
+
+ def rstrip_except_escaped(str, dn_index)
+ str_ends_with_whitespace = str.match(/\s\z/)
+
+ if str_ends_with_whitespace
+ dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
+
+ if dn_part_ends_with_escaped_whitespace
+ dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
+ num_chars_to_remove = dn_part_rwhitespace.length - 1
+ str = str[0, str.length - num_chars_to_remove]
+ else
+ str.rstrip!
+ end
+ end
+
+ str
+ end
+
+ ##
+ # Returns the DN as an array in the form expected by the constructor.
+ def to_a
+ a = []
+ self.each_pair { |key, value| a << key << value } unless @dn.empty?
+ a
+ end
+
+ ##
+ # Return the DN as an escaped string.
+ def to_s
+ @dn
+ end
+
+ ##
+ # Return the DN as an escaped and normalized string.
+ def to_normalized_s
+ self.class.new(*to_a).to_s.downcase
+ end
+
+ # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
+ # for DN values. All of the following must be escaped in any normal string
+ # using a single backslash ('\') as escape. The space character is left
+ # out here because in a "normalized" string, spaces should only be escaped
+ # if necessary (i.e. leading or trailing space).
+ NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
+
+ # The following must be represented as escaped hex
+ HEX_ESCAPES = {
+ "\n" => '\0a',
+ "\r" => '\0d'
+ }.freeze
+
+ # Compiled character class regexp using the keys from the above hash, and
+ # checking for a space or # at the start, or space at the end, of the
+ # string.
+ ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
+ NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ HEX_ESCAPE_RE = Regexp.new("([" +
+ HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ ##
+ # Escape a string for use in a DN value
+ def self.escape(string)
+ escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
+ escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
+ end
+
+ private
+
+ def initialize_array(args)
+ buffer = StringIO.new
+
+ args.each_with_index do |arg, index|
+ if index.even? # key
+ buffer << "," if index > 0
+ buffer << arg
+ else # value
+ buffer << "="
+ buffer << self.class.escape(arg)
+ end
+ end
+
+ @dn = buffer.string
+ end
+
+ def initialize_string(arg)
+ @dn = arg.to_s
+ end
+
+ ##
+ # Proxy all other requests to the string object, because a DN is mainly
+ # used within the library as a string
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @dn.send(method, *args, &block)
+ end
+
+ ##
+ # Redefined to be consistent with redefined `method_missing` behavior
+ def respond_to?(sym, include_private = false)
+ @dn.respond_to?(sym, include_private)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index 9a6f7827b16..38d7a9ba2f5 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -36,6 +36,26 @@ module Gitlab
]
end
+ def self.normalize_dn(dn)
+ ::Gitlab::LDAP::DN.new(dn).to_normalized_s
+ rescue ::Gitlab::LDAP::DN::FormatError => e
+ Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}")
+
+ dn
+ end
+
+ # Returns the UID in a normalized form.
+ #
+ # 1. Excess spaces are stripped
+ # 2. The string is downcased (for case-insensitivity)
+ def self.normalize_uid(uid)
+ ::Gitlab::LDAP::DN.normalize_value(uid)
+ rescue ::Gitlab::LDAP::DN::FormatError => e
+ Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}")
+
+ uid
+ end
+
def initialize(entry, provider)
Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
@entry = entry
@@ -58,7 +78,9 @@ module Gitlab
attribute_value(:email)
end
- delegate :dn, to: :entry
+ def dn
+ self.class.normalize_dn(entry.dn)
+ end
private
diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb
new file mode 100644
index 00000000000..0de0cddcce4
--- /dev/null
+++ b/lib/gitlab/middleware/read_only.rb
@@ -0,0 +1,88 @@
+module Gitlab
+ module Middleware
+ class ReadOnly
+ DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
+ APPLICATION_JSON = 'application/json'.freeze
+ API_VERSIONS = (3..4)
+
+ def initialize(app)
+ @app = app
+ @whitelisted = internal_routes
+ end
+
+ def call(env)
+ @env = env
+
+ if disallowed_request? && Gitlab::Database.read_only?
+ Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
+ error_message = 'You cannot do writing operations on a read-only GitLab instance'
+
+ if json_request?
+ return [403, { 'Content-Type' => 'application/json' }, [{ 'message' => error_message }.to_json]]
+ else
+ rack_flash.alert = error_message
+ rack_session['flash'] = rack_flash.to_session_value
+
+ return [301, { 'Location' => last_visited_url }, []]
+ end
+ end
+
+ @app.call(env)
+ end
+
+ private
+
+ def internal_routes
+ API_VERSIONS.flat_map { |version| "api/v#{version}/internal" }
+ end
+
+ def disallowed_request?
+ DISALLOWED_METHODS.include?(@env['REQUEST_METHOD']) && !whitelisted_routes
+ end
+
+ def json_request?
+ request.media_type == APPLICATION_JSON
+ end
+
+ def rack_flash
+ @rack_flash ||= ActionDispatch::Flash::FlashHash.from_session_value(rack_session)
+ end
+
+ def rack_session
+ @env['rack.session']
+ end
+
+ def request
+ @env['rack.request'] ||= Rack::Request.new(@env)
+ end
+
+ def last_visited_url
+ @env['HTTP_REFERER'] || rack_session['user_return_to'] || Rails.application.routes.url_helpers.root_url
+ end
+
+ def route_hash
+ @route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {}
+ end
+
+ def whitelisted_routes
+ logout_route || grack_route || @whitelisted.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
+ end
+
+ def logout_route
+ route_hash[:controller] == 'sessions' && route_hash[:action] == 'destroy'
+ end
+
+ def sidekiq_route
+ request.path.start_with?('/admin/sidekiq')
+ end
+
+ def grack_route
+ request.path.end_with?('.git/git-upload-pack')
+ end
+
+ def lfs_route
+ request.path.end_with?('/info/lfs/objects/batch')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 68815be4d13..47c2a422387 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -64,6 +64,8 @@ module Gitlab
protected
def add_or_update_user_identities
+ return unless gl_user
+
# find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index e2fbcefdb74..cc37d708ce1 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -33,6 +33,7 @@ module Gitlab
explore
favicon.ico
files
+ google_api
groups
health_check
help
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 58f6245579a..bd677ec4bf3 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -65,5 +65,9 @@ module Gitlab
"can contain only lowercase letters, digits, and '-'. " \
"Must start with a letter, and cannot end with '-'"
end
+
+ def build_trace_section_regex
+ @build_trace_section_regexp ||= /section_((?:start)|(?:end)):(\d+):([^\r]+)\r\033\[0K/.freeze
+ end
end
end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index a99f8e2b5f8..a37112ae5c4 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -222,10 +222,18 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def add_namespace(storage, name)
- path = full_path(storage, name)
- FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
+ Gitlab::GitalyClient.migrate(:add_namespace) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).add(name)
+ else
+ path = full_path(storage, name)
+ FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
+ end
+ end
rescue Errno::EEXIST => e
Rails.logger.warn("Directory exists as a file: #{e} at: #{path}")
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError, e.message
end
# Remove directory from repositories storage
@@ -236,7 +244,15 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def rm_namespace(storage, name)
- FileUtils.rm_r(full_path(storage, name), force: true)
+ Gitlab::GitalyClient.migrate(:remove_namespace) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).remove(name)
+ else
+ FileUtils.rm_r(full_path(storage, name), force: true)
+ end
+ end
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError, e.message
end
# Move namespace directory inside repositories storage
@@ -246,9 +262,17 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def mv_namespace(storage, old_name, new_name)
- return false if exists?(storage, new_name) || !exists?(storage, old_name)
+ Gitlab::GitalyClient.migrate(:rename_namespace) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).rename(old_name, new_name)
+ else
+ return false if exists?(storage, new_name) || !exists?(storage, old_name)
- FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
+ FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
+ end
+ end
+ rescue GRPC::InvalidArgument
+ false
end
def url_to_repo(path)
@@ -272,7 +296,13 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def exists?(storage, dir_name)
- File.exist?(full_path(storage, dir_name))
+ Gitlab::GitalyClient.migrate(:namespace_exists) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).exists?(dir_name)
+ else
+ File.exist?(full_path(storage, dir_name))
+ end
+ end
end
protected
@@ -349,6 +379,14 @@ module Gitlab
Bundler.with_original_env { Popen.popen(cmd, nil, vars) }
end
+ def gitaly_namespace_client(storage_path)
+ storage, _value = Gitlab.config.repositories.storages.find do |storage, value|
+ value['path'] == storage_path
+ end
+
+ Gitlab::GitalyClient::NamespaceService.new(storage)
+ end
+
def gitaly_migrate(method, &block)
Gitlab::GitalyClient.migrate(method, &block)
rescue GRPC::NotFound, GRPC::BadStatus => e
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 6857038dba8..3f3ba77d47f 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -48,6 +48,7 @@ module Gitlab
deploy_keys: DeployKey.count,
deployments: Deployment.count,
environments: ::Environment.count,
+ gcp_clusters: ::Gcp::Cluster.count,
in_review_folder: ::Environment.in_review_folder.count,
groups: Group.count,
issues: Issue.count,
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index f200c694562..58d5b0da1c4 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -103,11 +103,16 @@ module Gitlab
end
def send_git_diff(repository, diff_refs)
- params = {
- 'RepoPath' => repository.path_to_repo,
- 'ShaFrom' => diff_refs.base_sha,
- 'ShaTo' => diff_refs.head_sha
- }
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff)
+ {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'RawDiffRequest' => Gitaly::RawDiffRequest.new(
+ gitaly_diff_or_patch_hash(repository, diff_refs)
+ ).to_json
+ }
+ else
+ workhorse_diff_or_patch_hash(repository, diff_refs)
+ end
[
SEND_DATA_HEADER,
@@ -116,11 +121,16 @@ module Gitlab
end
def send_git_patch(repository, diff_refs)
- params = {
- 'RepoPath' => repository.path_to_repo,
- 'ShaFrom' => diff_refs.base_sha,
- 'ShaTo' => diff_refs.head_sha
- }
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch)
+ {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'RawPatchRequest' => Gitaly::RawPatchRequest.new(
+ gitaly_diff_or_patch_hash(repository, diff_refs)
+ ).to_json
+ }
+ else
+ workhorse_diff_or_patch_hash(repository, diff_refs)
+ end
[
SEND_DATA_HEADER,
@@ -216,6 +226,22 @@ module Gitlab
token: Gitlab::GitalyClient.token(repository.project.repository_storage)
}
end
+
+ def workhorse_diff_or_patch_hash(repository, diff_refs)
+ {
+ 'RepoPath' => repository.path_to_repo,
+ 'ShaFrom' => diff_refs.base_sha,
+ 'ShaTo' => diff_refs.head_sha
+ }
+ end
+
+ def gitaly_diff_or_patch_hash(repository, diff_refs)
+ {
+ repository: repository.gitaly_repository,
+ left_commit_id: diff_refs.base_sha,
+ right_commit_id: diff_refs.head_sha
+ }
+ end
end
end
end
diff --git a/lib/google_api/auth.rb b/lib/google_api/auth.rb
new file mode 100644
index 00000000000..99a82c849e0
--- /dev/null
+++ b/lib/google_api/auth.rb
@@ -0,0 +1,54 @@
+module GoogleApi
+ class Auth
+ attr_reader :access_token, :redirect_uri, :state
+
+ ConfigMissingError = Class.new(StandardError)
+
+ def initialize(access_token, redirect_uri, state: nil)
+ @access_token = access_token
+ @redirect_uri = redirect_uri
+ @state = state
+ end
+
+ def authorize_url
+ client.auth_code.authorize_url(
+ redirect_uri: redirect_uri,
+ scope: scope,
+ state: state # This is used for arbitary redirection
+ )
+ end
+
+ def get_token(code)
+ ret = client.auth_code.get_token(code, redirect_uri: redirect_uri)
+ return ret.token, ret.expires_at
+ end
+
+ protected
+
+ def scope
+ raise NotImplementedError
+ end
+
+ private
+
+ def config
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == "google_oauth2" }
+ end
+
+ def client
+ return @client if defined?(@client)
+
+ unless config
+ raise ConfigMissingError
+ end
+
+ @client = ::OAuth2::Client.new(
+ config.app_id,
+ config.app_secret,
+ site: 'https://accounts.google.com',
+ token_url: '/o/oauth2/token',
+ authorize_url: '/o/oauth2/auth'
+ )
+ end
+ end
+end
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
new file mode 100644
index 00000000000..a440a3e3562
--- /dev/null
+++ b/lib/google_api/cloud_platform/client.rb
@@ -0,0 +1,88 @@
+require 'google/apis/container_v1'
+
+module GoogleApi
+ module CloudPlatform
+ class Client < GoogleApi::Auth
+ DEFAULT_MACHINE_TYPE = 'n1-standard-1'.freeze
+ SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze
+ LEAST_TOKEN_LIFE_TIME = 10.minutes
+
+ class << self
+ def session_key_for_token
+ :cloud_platform_access_token
+ end
+
+ def session_key_for_expires_at
+ :cloud_platform_expires_at
+ end
+
+ def new_session_key_for_redirect_uri
+ SecureRandom.hex.tap do |state|
+ yield session_key_for_redirect_uri(state)
+ end
+ end
+
+ def session_key_for_redirect_uri(state)
+ "cloud_platform_second_redirect_uri_#{state}"
+ end
+ end
+
+ def scope
+ SCOPE
+ end
+
+ def validate_token(expires_at)
+ return false unless access_token
+ return false unless expires_at
+
+ # Making sure that the token will have been still alive during the cluster creation.
+ return false if token_life_time(expires_at) < LEAST_TOKEN_LIFE_TIME
+
+ true
+ end
+
+ def projects_zones_clusters_get(project_id, zone, cluster_id)
+ service = Google::Apis::ContainerV1::ContainerService.new
+ service.authorization = access_token
+
+ service.get_zone_cluster(project_id, zone, cluster_id)
+ end
+
+ def projects_zones_clusters_create(project_id, zone, cluster_name, cluster_size, machine_type:)
+ service = Google::Apis::ContainerV1::ContainerService.new
+ service.authorization = access_token
+
+ request_body = Google::Apis::ContainerV1::CreateClusterRequest.new(
+ {
+ "cluster": {
+ "name": cluster_name,
+ "initial_node_count": cluster_size,
+ "node_config": {
+ "machine_type": machine_type
+ }
+ }
+ } )
+
+ service.create_cluster(project_id, zone, request_body)
+ end
+
+ def projects_zones_operations(project_id, zone, operation_id)
+ service = Google::Apis::ContainerV1::ContainerService.new
+ service.authorization = access_token
+
+ service.get_zone_operation(project_id, zone, operation_id)
+ end
+
+ def parse_operation_id(self_link)
+ m = self_link.match(%r{projects/.*/zones/.*/operations/(.*)})
+ m[1] if m
+ end
+
+ private
+
+ def token_life_time(expires_at)
+ DateTime.strptime(expires_at, '%s').to_time.utc - Time.now.utc
+ end
+ end
+ end
+end
diff --git a/lib/rspec_flaky/config.rb b/lib/rspec_flaky/config.rb
new file mode 100644
index 00000000000..a17ae55910e
--- /dev/null
+++ b/lib/rspec_flaky/config.rb
@@ -0,0 +1,21 @@
+require 'json'
+
+module RspecFlaky
+ class Config
+ def self.generate_report?
+ ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true'
+ end
+
+ def self.suite_flaky_examples_report_path
+ ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/suite-report.json")
+ end
+
+ def self.flaky_examples_report_path
+ ENV['FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/report.json")
+ end
+
+ def self.new_flaky_examples_report_path
+ ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/new-report.json")
+ end
+ end
+end
diff --git a/lib/rspec_flaky/flaky_example.rb b/lib/rspec_flaky/flaky_example.rb
index f81fb90e870..6be24014d89 100644
--- a/lib/rspec_flaky/flaky_example.rb
+++ b/lib/rspec_flaky/flaky_example.rb
@@ -9,24 +9,21 @@ module RspecFlaky
line: example.line,
description: example.description,
last_attempts_count: example.attempts,
- flaky_reports: 1)
+ flaky_reports: 0)
else
super
end
end
- def first_flaky_at
- self[:first_flaky_at] || Time.now
- end
-
- def last_flaky_at
- Time.now
- end
+ def update_flakiness!(last_attempts_count: nil)
+ self.first_flaky_at ||= Time.now
+ self.last_flaky_at = Time.now
+ self.flaky_reports += 1
+ self.last_attempts_count = last_attempts_count if last_attempts_count
- def last_flaky_job
- return unless ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
-
- "#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
+ if ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
+ self.last_flaky_job = "#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
+ end
end
def to_h
diff --git a/lib/rspec_flaky/flaky_examples_collection.rb b/lib/rspec_flaky/flaky_examples_collection.rb
new file mode 100644
index 00000000000..973c95b0212
--- /dev/null
+++ b/lib/rspec_flaky/flaky_examples_collection.rb
@@ -0,0 +1,37 @@
+require 'json'
+
+module RspecFlaky
+ class FlakyExamplesCollection < SimpleDelegator
+ def self.from_json(json)
+ new(JSON.parse(json))
+ end
+
+ def initialize(collection = {})
+ unless collection.is_a?(Hash)
+ raise ArgumentError, "`collection` must be a Hash, #{collection.class} given!"
+ end
+
+ collection_of_flaky_examples =
+ collection.map do |uid, example|
+ [
+ uid,
+ example.is_a?(RspecFlaky::FlakyExample) ? example : RspecFlaky::FlakyExample.new(example)
+ ]
+ end
+
+ super(Hash[collection_of_flaky_examples])
+ end
+
+ def to_report
+ Hash[map { |uid, example| [uid, example.to_h] }].deep_symbolize_keys
+ end
+
+ def -(other)
+ unless other.respond_to?(:key)
+ raise ArgumentError, "`other` must respond to `#key?`, #{other.class} does not!"
+ end
+
+ self.class.new(reject { |uid, _| other.key?(uid) })
+ end
+ end
+end
diff --git a/lib/rspec_flaky/listener.rb b/lib/rspec_flaky/listener.rb
index ec2fbd9e36c..4a5bfec9967 100644
--- a/lib/rspec_flaky/listener.rb
+++ b/lib/rspec_flaky/listener.rb
@@ -2,11 +2,15 @@ require 'json'
module RspecFlaky
class Listener
- attr_reader :all_flaky_examples, :new_flaky_examples
-
- def initialize
- @new_flaky_examples = {}
- @all_flaky_examples = init_all_flaky_examples
+ # - suite_flaky_examples: contains all the currently tracked flacky example
+ # for the whole RSpec suite
+ # - flaky_examples: contains the examples detected as flaky during the
+ # current RSpec run
+ attr_reader :suite_flaky_examples, :flaky_examples
+
+ def initialize(suite_flaky_examples_json = nil)
+ @flaky_examples = FlakyExamplesCollection.new
+ @suite_flaky_examples = init_suite_flaky_examples(suite_flaky_examples_json)
end
def example_passed(notification)
@@ -14,29 +18,21 @@ module RspecFlaky
return unless current_example.attempts > 1
- flaky_example_hash = all_flaky_examples[current_example.uid]
-
- all_flaky_examples[current_example.uid] =
- if flaky_example_hash
- FlakyExample.new(flaky_example_hash).tap do |ex|
- ex.last_attempts_count = current_example.attempts
- ex.flaky_reports += 1
- end
- else
- FlakyExample.new(current_example).tap do |ex|
- new_flaky_examples[current_example.uid] = ex
- end
- end
+ flaky_example = suite_flaky_examples.fetch(current_example.uid) { FlakyExample.new(current_example) }
+ flaky_example.update_flakiness!(last_attempts_count: current_example.attempts)
+
+ flaky_examples[current_example.uid] = flaky_example
end
def dump_summary(_)
- write_report_file(all_flaky_examples, all_flaky_examples_report_path)
+ write_report_file(flaky_examples, RspecFlaky::Config.flaky_examples_report_path)
+ new_flaky_examples = flaky_examples - suite_flaky_examples
if new_flaky_examples.any?
Rails.logger.warn "\nNew flaky examples detected:\n"
- Rails.logger.warn JSON.pretty_generate(to_report(new_flaky_examples))
+ Rails.logger.warn JSON.pretty_generate(new_flaky_examples.to_report)
- write_report_file(new_flaky_examples, new_flaky_examples_report_path)
+ write_report_file(new_flaky_examples, RspecFlaky::Config.new_flaky_examples_report_path)
end
end
@@ -46,30 +42,23 @@ module RspecFlaky
private
- def init_all_flaky_examples
- return {} unless File.exist?(all_flaky_examples_report_path)
+ def init_suite_flaky_examples(suite_flaky_examples_json = nil)
+ unless suite_flaky_examples_json
+ return {} unless File.exist?(RspecFlaky::Config.suite_flaky_examples_report_path)
- all_flaky_examples = JSON.parse(File.read(all_flaky_examples_report_path))
+ suite_flaky_examples_json = File.read(RspecFlaky::Config.suite_flaky_examples_report_path)
+ end
- Hash[(all_flaky_examples || {}).map { |k, ex| [k, FlakyExample.new(ex)] }]
+ FlakyExamplesCollection.from_json(suite_flaky_examples_json)
end
- def write_report_file(examples, file_path)
- return unless ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true'
+ def write_report_file(examples_collection, file_path)
+ return unless RspecFlaky::Config.generate_report?
report_path_dir = File.dirname(file_path)
FileUtils.mkdir_p(report_path_dir) unless Dir.exist?(report_path_dir)
- File.write(file_path, JSON.pretty_generate(to_report(examples)))
- end
-
- def all_flaky_examples_report_path
- @all_flaky_examples_report_path ||= ENV['ALL_FLAKY_RSPEC_REPORT_PATH'] ||
- Rails.root.join("rspec_flaky/all-report.json")
- end
- def new_flaky_examples_report_path
- @new_flaky_examples_report_path ||= ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] ||
- Rails.root.join("rspec_flaky/new-report.json")
+ File.write(file_path, JSON.pretty_generate(examples_collection.to_report))
end
end
end
diff --git a/lib/system_check/app/git_user_default_ssh_config_check.rb b/lib/system_check/app/git_user_default_ssh_config_check.rb
index dfa8b8b3f5b..9af21078403 100644
--- a/lib/system_check/app/git_user_default_ssh_config_check.rb
+++ b/lib/system_check/app/git_user_default_ssh_config_check.rb
@@ -11,10 +11,10 @@ module SystemCheck
].freeze
set_name 'Git user has default SSH configuration?'
- set_skip_reason 'skipped (git user is not present or configured)'
+ set_skip_reason 'skipped (GitLab read-only, or git user is not present / configured)'
def skip?
- !home_dir || !File.directory?(home_dir)
+ Gitlab::Database.read_only? || !home_dir || !File.directory?(home_dir)
end
def check?
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 3eade7bf553..b4d05f5995a 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -4,7 +4,10 @@ namespace :gitlab do
task :ee_compat_check, [:branch] => :environment do |_, args|
opts =
if ENV['CI']
- { branch: ENV['CI_COMMIT_REF_NAME'] }
+ {
+ ce_repo: ENV['CI_REPOSITORY_URL'],
+ branch: ENV['CI_COMMIT_REF_NAME']
+ }
else
unless args[:branch]
puts "Must specify a branch as an argument".color(:red)
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 08677a98fc1..8377fe3269d 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -50,6 +50,8 @@ namespace :gitlab do
# only generate a configuration for the most common and simplest case: when
# we have exactly one Gitaly process and we are sure it is running locally
# because it uses a Unix socket.
+ # For development and testing purposes, an extra storage is added to gitaly,
+ # which is not known to Rails, but must be explicitly stubbed.
def gitaly_configuration_toml(gitaly_ruby: true)
storages = []
address = nil
@@ -67,6 +69,11 @@ namespace :gitlab do
storages << { name: key, path: val['path'] }
end
+
+ if Rails.env.test?
+ storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s }
+ end
+
config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages }
config[:auth] = { token: 'secret' } if Rails.env.test?
config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 4d485108cf6..7f86fd7b45e 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -39,13 +39,19 @@ class GithubImport
def import!
@project.force_import_start
+ import_success = false
+
timings = Benchmark.measure do
- Github::Import.new(@project, @options).execute
+ import_success = Github::Import.new(@project, @options).execute
end
- puts "Import finished. Timings: #{timings}".color(:green)
-
- @project.import_finish
+ if import_success
+ @project.import_finish
+ puts "Import finished. Timings: #{timings}".color(:green)
+ else
+ puts "Import was not successful. Errors were as follows:"
+ puts @project.import_error
+ end
end
def new_project
@@ -53,18 +59,23 @@ class GithubImport
namespace_path, _sep, name = @project_path.rpartition('/')
namespace = find_or_create_namespace(namespace_path)
- Projects::CreateService.new(
+ project = Projects::CreateService.new(
@current_user,
name: name,
path: name,
description: @repo['description'],
namespace_id: namespace.id,
visibility_level: visibility_level,
- import_type: 'github',
- import_source: @repo['full_name'],
- import_url: @repo['clone_url'].sub('://', "://#{@options[:token]}@"),
skip_wiki: @repo['has_wiki']
).execute
+
+ project.update!(
+ import_type: 'github',
+ import_source: @repo['full_name'],
+ import_url: @repo['clone_url'].sub('://', "://#{@options[:token]}@")
+ )
+
+ project
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e7b09ecfac1..1f356a231b0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-10 16:14+0200\n"
-"PO-Revision-Date: 2017-10-10 16:14+0200\n"
+"POT-Creation-Date: 2017-10-10 17:50+0200\n"
+"PO-Revision-Date: 2017-10-10 17:50+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -378,6 +378,132 @@ msgstr ""
msgid "Clone repository"
msgstr ""
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|Save"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -571,6 +697,9 @@ msgstr ""
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Don't show again"
msgstr ""
@@ -675,6 +804,9 @@ msgstr[1] ""
msgid "ForkedFromProjectPath|Forked from"
msgstr ""
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
msgid "Format"
msgstr ""
@@ -699,6 +831,9 @@ msgstr ""
msgid "GoToYourFork|Fork"
msgstr ""
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -836,9 +971,6 @@ msgstr[1] ""
msgid "Last Pipeline"
msgstr ""
-msgid "Last Update"
-msgstr ""
-
msgid "Last commit"
msgstr ""
@@ -848,6 +980,9 @@ msgstr ""
msgid "Last edited by %{name}"
msgstr ""
+msgid "Last update"
+msgstr ""
+
msgid "Last updated"
msgstr ""
@@ -877,6 +1012,15 @@ msgid_plural "Limited to showing %d events at most"
msgstr[0] ""
msgstr[1] ""
+msgid "Lock"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
+msgid "Login"
+msgstr ""
+
msgid "Median"
msgstr ""
@@ -904,6 +1048,9 @@ msgstr ""
msgid "More information is available|here"
msgstr ""
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
@@ -1020,9 +1167,15 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
+msgid "Only project members can comment."
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr ""
+msgid "Opens in a new window"
+msgstr ""
+
msgid "Options"
msgstr ""
@@ -1047,6 +1200,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr ""
+
msgid "Pipeline"
msgstr ""
@@ -1155,6 +1311,42 @@ msgstr ""
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr ""
@@ -1367,6 +1559,9 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
@@ -1558,12 +1753,24 @@ msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This is a confidential issue."
+msgstr ""
+
msgid "This is the author's first Merge Request to this project."
msgstr ""
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr ""
+msgid "This merge request is locked."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr ""
@@ -1718,6 +1925,12 @@ msgstr ""
msgid "Total test time for all commits/merges"
msgstr ""
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
msgid "Unstar"
msgstr ""
@@ -1880,9 +2093,15 @@ msgstr ""
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr ""
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr ""
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
@@ -1913,6 +2132,9 @@ msgstr ""
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr ""
+msgid "Your comment will not be visible to the public."
+msgstr ""
+
msgid "Your groups"
msgstr ""
@@ -1938,5 +2160,11 @@ msgid_plural "parents"
msgstr[0] ""
msgstr[1] ""
+msgid "password"
+msgstr ""
+
msgid "personal access token"
msgstr ""
+
+msgid "username"
+msgstr ""
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 95aeef10752..22d12b479cb 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -25,9 +25,9 @@ GEM
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
- mini_portile2 (2.1.0)
- nokogiri (1.7.0.1)
- mini_portile2 (~> 2.1.0)
+ mini_portile2 (2.3.0)
+ nokogiri (1.8.1)
+ mini_portile2 (~> 2.3.0)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@@ -75,4 +75,4 @@ DEPENDENCIES
selenium-webdriver (~> 2.53)
BUNDLED WITH
- 1.14.6
+ 1.15.4
diff --git a/qa/README.md b/qa/README.md
index e0ebb53a2e9..1cfbbdd9d42 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -1,10 +1,10 @@
-## Integration tests for GitLab
+# GitLab QA - Integration tests for GitLab
This directory contains integration tests for GitLab.
-It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa).
+It is part of the [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa).
-## What GitLab QA is?
+## What is it?
GitLab QA is an integration tests suite for GitLab.
@@ -20,18 +20,34 @@ against any existing instance.
## How can I use it?
You can use GitLab QA to exercise tests on any live instance! For example, the
-follow call would login to the local GitLab instance and run all specs in
+following call would login to a local [GDK] instance and run all specs in
`qa/specs/features`:
```
-GITLAB_USERNAME='root' GITLAB_PASSWORD='5iveL!fe' bin/qa Test::Instance http://localhost
+bin/qa Test::Instance http://localhost:3000
```
-You can also supply a specific tests to run as another parameter. For example, to
+### Running specific tests
+
+You can also supply specific tests to run as another parameter. For example, to
test the EE license specs, you can run:
```
-EE_LICENSE="<YOUR LICENSE KEY>" GITLAB_USERNAME='root' GITLAB_PASSWORD='5iveL!fe' bin/qa Test::Instance http://localhost qa/ee
+EE_LICENSE="<YOUR LICENSE KEY>" bin/qa Test::Instance http://localhost qa/ee
+```
+
+### Overriding the authenticated user
+
+Unless told otherwise, the QA tests will run as the default `root` user seeded
+by the GDK.
+
+If you need to authenticate as a different user, you can provide the
+`GITLAB_USERNAME` and `GITLAB_PASSWORD` environment variables:
+
+```
+GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance https://gitlab.example.com
```
All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa#supported-environment-variables).
+
+[GDK]: https://gitlab.com/gitlab-org/gitlab-development-kit/
diff --git a/qa/qa.rb b/qa/qa.rb
index db9d8c42fde..eb6f922d0d3 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -31,9 +31,17 @@ module QA
# GitLab instance scenarios.
#
module Gitlab
+ module Group
+ autoload :Create, 'qa/scenario/gitlab/group/create'
+ end
+
module Project
autoload :Create, 'qa/scenario/gitlab/project/create'
end
+
+ module Sandbox
+ autoload :Prepare, 'qa/scenario/gitlab/sandbox/prepare'
+ end
end
end
@@ -55,6 +63,7 @@ module QA
end
module Group
+ autoload :New, 'qa/page/group/new'
autoload :Show, 'qa/page/group/show'
end
diff --git a/qa/qa/page/dashboard/groups.rb b/qa/qa/page/dashboard/groups.rb
index 3690f40dcfe..083d2e1ab16 100644
--- a/qa/qa/page/dashboard/groups.rb
+++ b/qa/qa/page/dashboard/groups.rb
@@ -2,19 +2,22 @@ module QA
module Page
module Dashboard
class Groups < Page::Base
- def prepare_test_namespace
- if page.has_content?(Runtime::Namespace.name)
- return click_link(Runtime::Namespace.name)
- end
+ def filter_by_name(name)
+ fill_in 'Filter by name...', with: name
+ end
- click_on 'New group'
+ def has_group?(name)
+ filter_by_name(name)
+
+ page.has_link?(name)
+ end
- fill_in 'group_path', with: Runtime::Namespace.name
- fill_in 'group_description',
- with: "QA test run at #{Runtime::Namespace.time}"
- choose 'Private'
+ def go_to_group(name)
+ click_link name
+ end
- click_button 'Create group'
+ def go_to_new_group
+ click_on 'New group'
end
end
end
diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb
new file mode 100644
index 00000000000..cb743a7bf11
--- /dev/null
+++ b/qa/qa/page/group/new.rb
@@ -0,0 +1,23 @@
+module QA
+ module Page
+ module Group
+ class New < Page::Base
+ def set_path(path)
+ fill_in 'group_path', with: path
+ end
+
+ def set_description(description)
+ fill_in 'group_description', with: description
+ end
+
+ def set_visibility(visibility)
+ choose visibility
+ end
+
+ def create
+ click_button 'Create group'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index 296c311d7c6..6987c1f8f85 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -2,8 +2,24 @@ module QA
module Page
module Group
class Show < Page::Base
+ def go_to_subgroups
+ click_link 'Subgroups'
+ end
+
+ def go_to_subgroup(name)
+ click_link name
+ end
+
+ def has_subgroup?(name)
+ page.has_link?(name)
+ end
+
+ def go_to_new_subgroup
+ click_on 'New Subgroup'
+ end
+
def go_to_new_project
- click_link 'New Project'
+ click_on 'New Project'
end
end
end
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
index e4910b63a14..996286430b9 100644
--- a/qa/qa/runtime/namespace.rb
+++ b/qa/qa/runtime/namespace.rb
@@ -10,6 +10,10 @@ module QA
def name
'qa_test_' + time.strftime('%d_%m_%Y_%H-%M-%S')
end
+
+ def sandbox_name
+ 'gitlab-qa-sandbox'
+ end
end
end
end
diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb
index 12ceda015f0..60027c89ab1 100644
--- a/qa/qa/runtime/user.rb
+++ b/qa/qa/runtime/user.rb
@@ -8,7 +8,7 @@ module QA
end
def password
- ENV['GITLAB_PASSWORD'] || 'test1234'
+ ENV['GITLAB_PASSWORD'] || '5iveL!fe'
end
end
end
diff --git a/qa/qa/scenario/gitlab/group/create.rb b/qa/qa/scenario/gitlab/group/create.rb
new file mode 100644
index 00000000000..8e6c7c7ad80
--- /dev/null
+++ b/qa/qa/scenario/gitlab/group/create.rb
@@ -0,0 +1,27 @@
+require 'securerandom'
+
+module QA
+ module Scenario
+ module Gitlab
+ module Group
+ class Create < Scenario::Template
+ attr_writer :path, :description
+
+ def initialize
+ @path = Runtime::Namespace.name
+ @description = "QA test run at #{Runtime::Namespace.time}"
+ end
+
+ def perform
+ Page::Group::New.perform do |group|
+ group.set_path(@path)
+ group.set_description(@description)
+ group.set_visibility('Private')
+ group.create
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/gitlab/project/create.rb b/qa/qa/scenario/gitlab/project/create.rb
index b860701c304..7b614bfdd94 100644
--- a/qa/qa/scenario/gitlab/project/create.rb
+++ b/qa/qa/scenario/gitlab/project/create.rb
@@ -12,9 +12,23 @@ module QA
end
def perform
- Page::Main::Menu.act { go_to_groups }
- Page::Dashboard::Groups.act { prepare_test_namespace }
- Page::Group::Show.act { go_to_new_project }
+ Scenario::Gitlab::Sandbox::Prepare.perform
+
+ Page::Group::Show.perform do |page|
+ page.go_to_subgroups
+
+ if page.has_subgroup?(Runtime::Namespace.name)
+ page.go_to_subgroup(Runtime::Namespace.name)
+ else
+ page.go_to_new_subgroup
+
+ Scenario::Gitlab::Group::Create.perform do |group|
+ group.path = Runtime::Namespace.name
+ end
+ end
+
+ page.go_to_new_project
+ end
Page::Project::New.perform do |page|
page.choose_test_namespace
diff --git a/qa/qa/scenario/gitlab/sandbox/prepare.rb b/qa/qa/scenario/gitlab/sandbox/prepare.rb
new file mode 100644
index 00000000000..990de456e20
--- /dev/null
+++ b/qa/qa/scenario/gitlab/sandbox/prepare.rb
@@ -0,0 +1,28 @@
+module QA
+ module Scenario
+ module Gitlab
+ module Sandbox
+ # Ensure we're in our sandbox namespace, either by navigating to it or
+ # by creating it if it doesn't yet exist
+ class Prepare < Scenario::Template
+ def perform
+ Page::Main::Menu.act { go_to_groups }
+
+ Page::Dashboard::Groups.perform do |page|
+ if page.has_group?(Runtime::Namespace.sandbox_name)
+ page.go_to_group(Runtime::Namespace.sandbox_name)
+ else
+ page.go_to_new_group
+
+ Scenario::Gitlab::Group::Create.perform do |group|
+ group.path = Runtime::Namespace.sandbox_name
+ group.description = 'QA sandbox'
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/datetime.rb b/rubocop/cop/migration/datetime.rb
index 651935dd53e..9cba3c35b26 100644
--- a/rubocop/cop/migration/datetime.rb
+++ b/rubocop/cop/migration/datetime.rb
@@ -7,14 +7,18 @@ module RuboCop
class Datetime < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = 'Do not use the `datetime` data type, use `datetime_with_timezone` instead'.freeze
+ MSG = 'Do not use the `%s` data type, use `datetime_with_timezone` instead'.freeze
# Check methods in table creation.
def on_def(node)
return unless in_migration?(node)
node.each_descendant(:send) do |send_node|
- add_offense(send_node, :selector) if method_name(send_node) == :datetime
+ method_name = node.children[1]
+
+ if method_name == :datetime || method_name == :timestamp
+ add_offense(send_node, :selector, format(MSG, method_name))
+ end
end
end
@@ -23,12 +27,14 @@ module RuboCop
return unless in_migration?(node)
node.each_descendant do |descendant|
- add_offense(node, :expression) if descendant.type == :sym && descendant.children.last == :datetime
- end
- end
+ next unless descendant.type == :sym
- def method_name(node)
- node.children[1]
+ last_argument = descendant.children.last
+
+ if last_argument == :datetime || last_argument == :timestamp
+ add_offense(node, :expression, format(MSG, last_argument))
+ end
+ end
end
end
end
diff --git a/rubocop/cop/rspec/verbose_include_metadata.rb b/rubocop/cop/rspec/verbose_include_metadata.rb
new file mode 100644
index 00000000000..58390622d60
--- /dev/null
+++ b/rubocop/cop/rspec/verbose_include_metadata.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'rubocop-rspec'
+
+module RuboCop
+ module Cop
+ module RSpec
+ # Checks for verbose include metadata used in the specs.
+ #
+ # @example
+ # # bad
+ # describe MyClass, js: true do
+ # end
+ #
+ # # good
+ # describe MyClass, :js do
+ # end
+ class VerboseIncludeMetadata < Cop
+ MSG = 'Use `%s` instead of `%s`.'
+
+ SELECTORS = %i[describe context feature example_group it specify example scenario its].freeze
+
+ def_node_matcher :include_metadata, <<-PATTERN
+ (send {(const nil :RSpec) nil} {#{SELECTORS.map(&:inspect).join(' ')}}
+ !const
+ ...
+ (hash $...))
+ PATTERN
+
+ def_node_matcher :invalid_metadata?, <<-PATTERN
+ (pair
+ (sym $...)
+ (true))
+ PATTERN
+
+ def on_send(node)
+ invalid_metadata_matches(node) do |match|
+ add_offense(node, :expression, format(MSG, good(match), bad(match)))
+ end
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ invalid_metadata_matches(node) do |match|
+ corrector.replace(match.loc.expression, good(match))
+ end
+ end
+ end
+
+ private
+
+ def invalid_metadata_matches(node)
+ include_metadata(node) do |matches|
+ matches.select(&method(:invalid_metadata?)).each do |match|
+ yield match
+ end
+ end
+ end
+
+ def bad(match)
+ "#{metadata_key(match)}: true"
+ end
+
+ def good(match)
+ ":#{metadata_key(match)}"
+ end
+
+ def metadata_key(match)
+ match.children[0].source
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 1b6e8991a17..1df23899efb 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -21,3 +21,4 @@ require_relative 'cop/migration/reversible_add_column_with_default'
require_relative 'cop/migration/timestamps'
require_relative 'cop/migration/update_column_in_batches'
require_relative 'cop/rspec/single_line_hook'
+require_relative 'cop/rspec/verbose_include_metadata'
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index aadd3317875..25fe547ff37 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Admin::UsersController do
let(:user) { create(:user) }
- let(:admin) { create(:admin) }
+ set(:admin) { create(:admin) }
before do
sign_in(admin)
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index c8c6b9f41bf..9df4ebf2fa0 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -57,7 +57,7 @@ describe Dashboard::TodosController do
expect(response).to redirect_to(dashboard_todos_path(page: last_page))
end
- it 'redirects to correspondent page' do
+ it 'goes to the correct page' do
get :index, page: last_page
expect(assigns(:todos).current_page).to eq(last_page)
@@ -70,6 +70,30 @@ describe Dashboard::TodosController do
expect(response).to redirect_to(dashboard_todos_path(page: last_page))
end
+
+ context 'when providing no filters' do
+ it 'does not perform a query to get the page count, but gets that from the user' do
+ allow(controller).to receive(:current_user).and_return(user)
+
+ expect(user).to receive(:todos_pending_count).and_call_original
+
+ get :index, page: (last_page + 1).to_param, sort: :created_asc
+
+ expect(response).to redirect_to(dashboard_todos_path(page: last_page, sort: :created_asc))
+ end
+ end
+
+ context 'when providing filters' do
+ it 'performs a query to get the correct page count' do
+ allow(controller).to receive(:current_user).and_return(user)
+
+ expect(user).not_to receive(:todos_pending_count)
+
+ get :index, page: (last_page + 1).to_param, project_id: project.id
+
+ expect(response).to redirect_to(dashboard_todos_path(page: last_page, project_id: project.id))
+ end
+ end
end
end
diff --git a/spec/controllers/google_api/authorizations_controller_spec.rb b/spec/controllers/google_api/authorizations_controller_spec.rb
new file mode 100644
index 00000000000..80d553f0f34
--- /dev/null
+++ b/spec/controllers/google_api/authorizations_controller_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe GoogleApi::AuthorizationsController do
+ describe 'GET|POST #callback' do
+ let(:user) { create(:user) }
+ let(:token) { 'token' }
+ let(:expires_at) { 1.hour.since.strftime('%s') }
+
+ subject { get :callback, code: 'xxx', state: @state }
+
+ before do
+ sign_in(user)
+
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:get_token).and_return([token, expires_at])
+ end
+
+ it 'sets token and expires_at in session' do
+ subject
+
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token])
+ .to eq(token)
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at])
+ .to eq(expires_at)
+ end
+
+ context 'when redirect uri key is stored in state' do
+ set(:project) { create(:project) }
+ let(:redirect_uri) { project_clusters_url(project).to_s }
+
+ before do
+ @state = GoogleApi::CloudPlatform::Client
+ .new_session_key_for_redirect_uri do |key|
+ session[key] = redirect_uri
+ end
+ end
+
+ it 'redirects to the URL stored in state param' do
+ expect(subject).to redirect_to(redirect_uri)
+ end
+ end
+
+ context 'when redirection url is not stored in state' do
+ it 'redirects to root_path' do
+ expect(subject).to redirect_to(root_path)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/profiles/emails_controller_spec.rb b/spec/controllers/profiles/emails_controller_spec.rb
new file mode 100644
index 00000000000..ecf14aad54f
--- /dev/null
+++ b/spec/controllers/profiles/emails_controller_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Profiles::EmailsController do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe '#create' do
+ let(:email_params) { { email: "add_email@example.com" } }
+
+ it 'sends an email confirmation' do
+ expect { post(:create, { email: email_params }) }.to change { ActionMailer::Base.deliveries.size }
+ expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions"
+ end
+ end
+
+ describe '#resend_confirmation_instructions' do
+ let(:email_params) { { email: "add_email@example.com" } }
+
+ it 'resends an email confirmation' do
+ email = user.emails.create(email: 'add_email@example.com')
+
+ expect { put(:resend_confirmation_instructions, { id: email }) }.to change { ActionMailer::Base.deliveries.size }
+ expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions"
+ end
+
+ it 'unable to resend an email confirmation' do
+ expect { put(:resend_confirmation_instructions, { id: 1 }) }.not_to change { ActionMailer::Base.deliveries.size }
+ end
+ end
+end
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index b52b63e05a4..ce5040ff02b 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -15,6 +15,20 @@ describe ProfilesController do
expect(user.unconfirmed_email).to eq('john@gmail.com')
end
+ it "allows an email update without confirmation if existing verified email" do
+ user = create(:user)
+ create(:email, :confirmed, user: user, email: 'john@gmail.com')
+ sign_in(user)
+
+ put :update,
+ user: { email: "john@gmail.com", name: "John" }
+
+ user.reload
+
+ expect(response.status).to eq(302)
+ expect(user.unconfirmed_email).to eq nil
+ end
+
it "ignores an email update from a user with an external email address" do
stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
stub_omniauth_setting(sync_profile_attributes: true)
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index caa63e7bd22..d0992719171 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe Projects::ArtifactsController do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :repository, :public) }
let(:pipeline) do
create(:ci_pipeline,
@@ -15,7 +15,7 @@ describe Projects::ArtifactsController do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
before do
- project.team << [user, :developer]
+ project.add_developer(user)
sign_in(user)
end
@@ -47,19 +47,67 @@ describe Projects::ArtifactsController do
end
describe 'GET file' do
- context 'when the file exists' do
- it 'renders the file view' do
- get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ end
- expect(response).to render_template('projects/artifacts/file')
+ context 'when the file is served by GitLab Pages' do
+ before do
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ context 'when the file exists' do
+ it 'renders the file view' do
+ get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
+
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'when the file does not exist' do
+ it 'responds Not Found' do
+ get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
+
+ expect(response).to be_not_found
+ end
end
end
- context 'when the file does not exist' do
- it 'responds Not Found' do
- get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
+ context 'when the file is served through Rails' do
+ context 'when the file exists' do
+ it 'renders the file view' do
+ get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
- expect(response).to be_not_found
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template('projects/artifacts/file')
+ end
+ end
+
+ context 'when the file does not exist' do
+ it 'responds Not Found' do
+ get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
+
+ expect(response).to be_not_found
+ end
+ end
+ end
+
+ context 'when the project is private' do
+ let(:private_project) { create(:project, :repository, :private) }
+ let(:pipeline) { create(:ci_pipeline, project: private_project) }
+ let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+
+ before do
+ private_project.add_developer(user)
+
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ it 'does not redirect the request' do
+ get :file, namespace_id: private_project.namespace, project_id: private_project, job_id: job, path: 'ci_artifacts.txt'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template('projects/artifacts/file')
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 64b9af7b845..fb76b7fdf38 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
describe Projects::BlobController do
+ include ProjectForksHelper
+
let(:project) { create(:project, :public, :repository) }
describe "GET show" do
@@ -226,9 +228,8 @@ describe Projects::BlobController do
end
context 'when user has forked project' do
- let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) }
- let!(:forked_project) { forked_project_link.forked_to_project }
- let(:guest) { forked_project.owner }
+ let!(:forked_project) { fork_project(project, guest, namespace: guest.namespace, repository: true) }
+ let(:guest) { create(:user) }
before do
sign_in(guest)
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 5e0b57e9b2e..3b3b63444c7 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -62,7 +62,7 @@ describe Projects::BranchesController do
let(:branch) { "feature%2Ftest" }
let(:ref) { "<script>alert('ref');</script>" }
it { is_expected.to render_template('new') }
- it { project.repository.branch_names.include?('feature/test') }
+ it { project.repository.branch_exists?('feature/test') }
end
end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
new file mode 100644
index 00000000000..7985028d73b
--- /dev/null
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -0,0 +1,308 @@
+require 'spec_helper'
+
+describe Projects::ClustersController do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ let(:role) { :master }
+
+ before do
+ project.team << [user, role]
+
+ sign_in(user)
+ end
+
+ describe 'GET index' do
+ subject do
+ get :index, namespace_id: project.namespace,
+ project_id: project
+ end
+
+ context 'when cluster is already created' do
+ let!(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ it 'redirects to show a cluster' do
+ subject
+
+ expect(response).to redirect_to(project_cluster_path(project, cluster))
+ end
+ end
+
+ context 'when we do not have cluster' do
+ it 'redirects to create a cluster' do
+ subject
+
+ expect(response).to redirect_to(new_project_cluster_path(project))
+ end
+ end
+ end
+
+ describe 'GET login' do
+ render_views
+
+ subject do
+ get :login, namespace_id: project.namespace,
+ project_id: project
+ end
+
+ context 'when we do have omniauth configured' do
+ it 'shows login button' do
+ subject
+
+ expect(response.body).to include('auth_buttons/signin_with_google')
+ end
+ end
+
+ context 'when we do not have omniauth configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'shows notice message' do
+ subject
+
+ expect(response.body).to include('Ask your GitLab administrator if you want to use this service.')
+ end
+ end
+ end
+
+ shared_examples 'requires to login' do
+ it 'redirects to create a cluster' do
+ subject
+
+ expect(response).to redirect_to(login_project_clusters_path(project))
+ end
+ end
+
+ describe 'GET new' do
+ render_views
+
+ subject do
+ get :new, namespace_id: project.namespace,
+ project_id: project
+ end
+
+ context 'when logged' do
+ before do
+ make_logged_in
+ end
+
+ it 'shows a creation form' do
+ subject
+
+ expect(response.body).to include('Create cluster')
+ end
+ end
+
+ context 'when not logged' do
+ it_behaves_like 'requires to login'
+ end
+ end
+
+ describe 'POST create' do
+ subject do
+ post :create, params.merge(namespace_id: project.namespace,
+ project_id: project)
+ end
+
+ context 'when not logged' do
+ let(:params) { {} }
+
+ it_behaves_like 'requires to login'
+ end
+
+ context 'when logged in' do
+ before do
+ make_logged_in
+ end
+
+ context 'when all required parameters are set' do
+ let(:params) do
+ {
+ cluster: {
+ gcp_cluster_name: 'new-cluster',
+ gcp_project_id: '111'
+ }
+ }
+ end
+
+ before do
+ expect(ClusterProvisionWorker).to receive(:perform_async) { }
+ end
+
+ it 'creates a new cluster' do
+ expect { subject }.to change { Gcp::Cluster.count }
+
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ end
+ end
+
+ context 'when not all required parameters are set' do
+ render_views
+
+ let(:params) do
+ {
+ cluster: {
+ project_namespace: 'some namespace'
+ }
+ }
+ end
+
+ it 'shows an error message' do
+ expect { subject }.not_to change { Gcp::Cluster.count }
+
+ expect(response).to render_template(:new)
+ end
+ end
+ end
+ end
+
+ describe 'GET status' do
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ subject do
+ get :status, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster,
+ format: :json
+ end
+
+ it "responds with matching schema" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('cluster_status')
+ end
+ end
+
+ describe 'GET show' do
+ render_views
+
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ subject do
+ get :show, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
+ end
+
+ context 'when logged as master' do
+ it "allows to update cluster" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Save")
+ end
+
+ it "allows remove integration" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Remove integration")
+ end
+ end
+
+ context 'when logged as developer' do
+ let(:role) { :developer }
+
+ it "does not allow to access page" do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'PUT update' do
+ render_views
+
+ let(:service) { project.build_kubernetes_service }
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project, service: service) }
+ let(:params) { {} }
+
+ subject do
+ put :update, params.merge(namespace_id: project.namespace,
+ project_id: project,
+ id: cluster)
+ end
+
+ context 'when logged as master' do
+ context 'when valid params are used' do
+ let(:params) do
+ {
+ cluster: { enabled: false }
+ }
+ end
+
+ it "redirects back to show page" do
+ subject
+
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ end
+ end
+
+ context 'when invalid params are used' do
+ let(:params) do
+ {
+ cluster: { project_namespace: 'my Namespace 321321321 #' }
+ }
+ end
+
+ it "rejects changes" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+ end
+ end
+
+ context 'when logged as developer' do
+ let(:role) { :developer }
+
+ it "does not allow to update cluster" do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'delete update' do
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ subject do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
+ end
+
+ context 'when logged as master' do
+ it "redirects back to clusters list" do
+ subject
+
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
+ end
+
+ context 'when logged as developer' do
+ let(:role) { :developer }
+
+ it "does not allow to destroy cluster" do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ def make_logged_in
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token] = '1234'
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = in_hour.to_i.to_s
+ end
+
+ def in_hour
+ Time.now + 1.hour
+ end
+end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index fad2c8f3ab7..7260350d5fb 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::MergeRequests::DiffsController do
+ include ProjectForksHelper
+
let(:project) { create(:project, :repository) }
let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -37,12 +39,12 @@ describe Projects::MergeRequests::DiffsController do
render_views
let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:forked_project_with_submodules) }
- let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
+ let(:forked_project) { fork_project_with_submodules(project) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
before do
- fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- fork_project.save
+ project.add_developer(user)
+
merge_request.reload
go
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index e46d1995498..707e7c32283 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::MergeRequestsController do
+ include ProjectForksHelper
+
let(:project) { create(:project, :repository) }
let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -204,14 +206,11 @@ describe Projects::MergeRequestsController do
context 'there is no source project' do
let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:forked_project_with_submodules) }
- let(:merge_request) { create(:merge_request, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
+ let(:forked_project) { fork_project_with_submodules(project) }
+ let!(:merge_request) { create(:merge_request, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
before do
- fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- fork_project.save
- merge_request.reload
- fork_project.destroy
+ forked_project.destroy
end
it 'closes MR without errors' do
@@ -599,21 +598,16 @@ describe Projects::MergeRequestsController do
describe 'GET ci_environments_status' do
context 'the environment is from a forked project' do
- let!(:forked) { create(:project, :repository) }
+ let!(:forked) { fork_project(project, user, repository: true) }
let!(:environment) { create(:environment, project: forked) }
let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
let(:admin) { create(:admin) }
let(:merge_request) do
- create(:forked_project_link, forked_to_project: forked,
- forked_from_project: project)
-
create(:merge_request, source_project: forked, target_project: project)
end
before do
- forked.team << [user, :master]
-
get :ci_environments_status,
namespace_id: merge_request.project.namespace.to_param,
project_id: merge_request.project,
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index c0337f96fc6..135fd6449ff 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::NotesController do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
@@ -210,18 +212,16 @@ describe Projects::NotesController do
context 'when creating a commit comment from an MR fork' do
let(:project) { create(:project, :repository) }
- let(:fork_project) do
- create(:project, :repository).tap do |fork|
- create(:forked_project_link, forked_to_project: fork, forked_from_project: project)
- end
+ let(:forked_project) do
+ fork_project(project, nil, repository: true)
end
let(:merge_request) do
- create(:merge_request, source_project: fork_project, target_project: project, source_branch: 'feature', target_branch: 'master')
+ create(:merge_request, source_project: forked_project, target_project: project, source_branch: 'feature', target_branch: 'master')
end
let(:existing_comment) do
- create(:note_on_commit, note: 'a note', project: fork_project, commit_id: merge_request.commit_shas.first)
+ create(:note_on_commit, note: 'a note', project: forked_project, commit_id: merge_request.commit_shas.first)
end
def post_create(extra_params = {})
@@ -231,7 +231,7 @@ describe Projects::NotesController do
project_id: project,
target_type: 'merge_request',
target_id: merge_request.id,
- note_project_id: fork_project.id,
+ note_project_id: forked_project.id,
in_reply_to_discussion_id: existing_comment.discussion_id
}.merge(extra_params)
end
@@ -253,16 +253,66 @@ describe Projects::NotesController do
end
context 'when the user has access to the fork' do
- let(:discussion) { fork_project.notes.find_discussion(existing_comment.discussion_id) }
+ let(:discussion) { forked_project.notes.find_discussion(existing_comment.discussion_id) }
before do
- fork_project.add_developer(user)
+ forked_project.add_developer(user)
existing_comment
end
it 'creates the note' do
- expect { post_create }.to change { fork_project.notes.count }.by(1)
+ expect { post_create }.to change { forked_project.notes.count }.by(1)
+ end
+ end
+ end
+
+ context 'when the merge request discussion is locked' do
+ before do
+ project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ merge_request.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when a noteable is not found' do
+ it 'returns 404 status' do
+ request_params[:note][:noteable_id] = 9999
+ post :create, request_params.merge(format: :json)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when a user is a team member' do
+ it 'returns 302 status for html' do
+ post :create, request_params
+
+ expect(response).to have_http_status(302)
+ end
+
+ it 'returns 200 status for json' do
+ post :create, request_params.merge(format: :json)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'creates a new note' do
+ expect { post :create, request_params }.to change { Note.count }.by(1)
+ end
+ end
+
+ context 'when a user is not a team member' do
+ before do
+ project.project_member(user).destroy
+ end
+
+ it 'returns 404 status' do
+ post :create, request_params
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'does not create a new note' do
+ expect { post :create, request_params }.not_to change { Note.count }
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 2a91a6613e6..0544afe31ed 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -1,6 +1,8 @@
require('spec_helper')
describe ProjectsController do
+ include ProjectForksHelper
+
let(:project) { create(:project) }
let(:public_project) { create(:project, :public) }
let(:user) { create(:user) }
@@ -139,8 +141,9 @@ describe ProjectsController do
end
end
- context 'when the storage is not available', broken_storage: true do
- let(:project) { create(:project, :broken_storage) }
+ context 'when the storage is not available', :broken_storage do
+ set(:project) { create(:project, :broken_storage) }
+
before do
project.add_developer(user)
sign_in(user)
@@ -376,10 +379,10 @@ describe ProjectsController do
context "when the project is forked" do
let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:project, :repository, forked_from_project: project) }
+ let(:forked_project) { fork_project(project, nil, repository: true) }
let(:merge_request) do
create(:merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
@@ -387,7 +390,7 @@ describe ProjectsController do
project.merge_requests << merge_request
sign_in(admin)
- delete :destroy, namespace_id: fork_project.namespace, id: fork_project
+ delete :destroy, namespace_id: forked_project.namespace, id: forked_project
expect(merge_request.reload.state).to eq('closed')
end
@@ -454,18 +457,14 @@ describe ProjectsController do
end
context 'with forked project' do
- let(:project_fork) { create(:project, :repository, namespace: user.namespace) }
-
- before do
- create(:forked_project_link, forked_to_project: project_fork)
- end
+ let(:forked_project) { fork_project(create(:project, :public), user) }
it 'removes fork from project' do
delete(:remove_fork,
- namespace_id: project_fork.namespace.to_param,
- id: project_fork.to_param, format: :js)
+ namespace_id: forked_project.namespace.to_param,
+ id: forked_project.to_param, format: :js)
- expect(project_fork.forked?).to be_falsey
+ expect(forked_project.reload.forked?).to be_falsey
expect(flash[:notice]).to eq('The fork relationship has been removed.')
expect(response).to render_template(:remove_fork)
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 5a4ab39ab86..1d3ddfbd220 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -76,12 +76,68 @@ describe RegistrationsController do
sign_in(user)
end
- it 'schedules the user for destruction' do
- expect(DeleteUserWorker).to receive(:perform_async).with(user.id, user.id, {})
+ def expect_failure(message)
+ expect(flash[:alert]).to eq(message)
+ expect(response.status).to eq(303)
+ expect(response).to redirect_to profile_account_path
+ end
+
+ def expect_password_failure
+ expect_failure('Invalid password')
+ end
+
+ def expect_username_failure
+ expect_failure('Invalid username')
+ end
+
+ def expect_success
+ expect(flash[:notice]).to eq 'Account scheduled for removal.'
+ expect(response.status).to eq(303)
+ expect(response).to redirect_to new_user_session_path
+ end
- post(:destroy)
+ context 'user requires password confirmation' do
+ it 'fails if password confirmation is not provided' do
+ post :destroy
- expect(response.status).to eq(302)
+ expect_password_failure
+ end
+
+ it 'fails if password confirmation is wrong' do
+ post :destroy, password: 'wrong password'
+
+ expect_password_failure
+ end
+
+ it 'succeeds if password is confirmed' do
+ post :destroy, password: '12345678'
+
+ expect_success
+ end
+ end
+
+ context 'user does not require password confirmation' do
+ before do
+ stub_application_setting(password_authentication_enabled: false)
+ end
+
+ it 'fails if username confirmation is not provided' do
+ post :destroy
+
+ expect_username_failure
+ end
+
+ it 'fails if username confirmation is wrong' do
+ post :destroy, username: 'wrong username'
+
+ expect_username_failure
+ end
+
+ it 'succeeds if username is confirmed' do
+ post :destroy, username: user.username
+
+ expect_success
+ end
end
end
end
diff --git a/spec/factories/ci/build_trace_section_names.rb b/spec/factories/ci/build_trace_section_names.rb
new file mode 100644
index 00000000000..1c16225f0e5
--- /dev/null
+++ b/spec/factories/ci/build_trace_section_names.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :ci_build_trace_section_name, class: Ci::BuildTraceSectionName do
+ sequence(:name) { |n| "section_#{n}" }
+ project factory: :project
+ end
+end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index e5ea6b41ea3..f994c2df821 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -47,6 +47,7 @@ FactoryGirl.define do
trait :invalid do
config(rspec: nil)
+ failure_reason :config_error
end
trait :blocked do
diff --git a/spec/factories/emails.rb b/spec/factories/emails.rb
index 8303861bcfe..c9ab87a15aa 100644
--- a/spec/factories/emails.rb
+++ b/spec/factories/emails.rb
@@ -2,5 +2,7 @@ FactoryGirl.define do
factory :email do
user
email { generate(:email_alias) }
+
+ trait(:confirmed) { confirmed_at Time.now }
end
end
diff --git a/spec/factories/fork_networks.rb b/spec/factories/fork_networks.rb
new file mode 100644
index 00000000000..f42d36f3d19
--- /dev/null
+++ b/spec/factories/fork_networks.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :fork_network do
+ association :root_project, factory: :project
+ end
+end
diff --git a/spec/factories/gcp/cluster.rb b/spec/factories/gcp/cluster.rb
new file mode 100644
index 00000000000..630e40da888
--- /dev/null
+++ b/spec/factories/gcp/cluster.rb
@@ -0,0 +1,38 @@
+FactoryGirl.define do
+ factory :gcp_cluster, class: Gcp::Cluster do
+ project
+ user
+ enabled true
+ gcp_project_id 'gcp-project-12345'
+ gcp_cluster_name 'test-cluster'
+ gcp_cluster_zone 'us-central1-a'
+ gcp_cluster_size 1
+ gcp_machine_type 'n1-standard-4'
+
+ trait :with_kubernetes_service do
+ after(:create) do |cluster, evaluator|
+ create(:kubernetes_service, project: cluster.project).tap do |service|
+ cluster.update(service: service)
+ end
+ end
+ end
+
+ trait :custom_project_namespace do
+ project_namespace 'sample-app'
+ end
+
+ trait :created_on_gke do
+ status_event :make_created
+ endpoint '111.111.111.111'
+ ca_cert 'xxxxxx'
+ kubernetes_token 'xxxxxx'
+ username 'xxxxxx'
+ password 'xxxxxx'
+ end
+
+ trait :errored do
+ status_event :make_errored
+ status_reason 'general error'
+ end
+ end
+end
diff --git a/spec/factories/gpg_key_subkeys.rb b/spec/factories/gpg_key_subkeys.rb
new file mode 100644
index 00000000000..66ecb44d84b
--- /dev/null
+++ b/spec/factories/gpg_key_subkeys.rb
@@ -0,0 +1,10 @@
+require_relative '../support/gpg_helpers'
+
+FactoryGirl.define do
+ factory :gpg_key_subkey do
+ gpg_key
+
+ sequence(:keyid) { |n| "keyid-#{n}" }
+ sequence(:fingerprint) { |n| "fingerprint-#{n}" }
+ end
+end
diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb
index 1258dce8940..93218e5763e 100644
--- a/spec/factories/gpg_keys.rb
+++ b/spec/factories/gpg_keys.rb
@@ -4,5 +4,9 @@ FactoryGirl.define do
factory :gpg_key do
key GpgHelpers::User1.public_key
user
+
+ factory :gpg_key_with_subkeys do
+ key GpgHelpers::User1.public_key_with_extra_signing_key
+ end
end
end
diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb
index c0beecf0bea..e9798ff6a41 100644
--- a/spec/factories/gpg_signature.rb
+++ b/spec/factories/gpg_signature.rb
@@ -5,7 +5,7 @@ FactoryGirl.define do
commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
project
gpg_key
- gpg_key_primary_keyid { gpg_key.primary_keyid }
+ gpg_key_primary_keyid { gpg_key.keyid }
verification_status :verified
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index cbec716d6ea..2c732aaf4ed 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -22,6 +22,11 @@ FactoryGirl.define do
trait :with_diffs do
end
+ trait :with_image_diffs do
+ source_branch "add_images_and_changes"
+ target_branch "master"
+ end
+
trait :without_diffs do
source_branch "improve/awesome"
target_branch "master"
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 958d62181a2..4034e7905ad 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -149,7 +149,7 @@ FactoryGirl.define do
end
end
- trait :readonly do
+ trait :read_only do
repository_read_only true
end
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 2144f6ba635..766cd4b0090 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Admin::AbuseReports", js: true do
+describe "Admin::AbuseReports", :js do
let(:user) { create(:user) }
context 'as an admin' do
diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb
index cbccf370514..9cb351282a0 100644
--- a/spec/features/admin/admin_broadcast_messages_spec.rb
+++ b/spec/features/admin/admin_broadcast_messages_spec.rb
@@ -40,7 +40,7 @@ feature 'Admin Broadcast Messages' do
expect(page).not_to have_content 'Migration to new server'
end
- scenario 'Live preview a customized broadcast message', js: true do
+ scenario 'Live preview a customized broadcast message', :js do
fill_in 'broadcast_message_message', with: "Live **Markdown** previews. :tada:"
page.within('.broadcast-message-preview') do
diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb
index e214ae6b78d..6a97378391b 100644
--- a/spec/features/admin/admin_disables_two_factor_spec.rb
+++ b/spec/features/admin/admin_disables_two_factor_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
feature 'Admin disables 2FA for a user' do
- scenario 'successfully', js: true do
+ scenario 'successfully', :js do
sign_in(create(:admin))
user = create(:user, :two_factor)
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 3768727d8ae..771fb5253da 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -52,7 +52,7 @@ feature 'Admin Groups' do
expect_selected_visibility(internal)
end
- scenario 'when entered in group path, it auto filled the group name', js: true do
+ scenario 'when entered in group path, it auto filled the group name', :js do
visit admin_groups_path
click_link "New group"
group_path = 'gitlab'
@@ -81,7 +81,7 @@ feature 'Admin Groups' do
expect_selected_visibility(group.visibility_level)
end
- scenario 'edit group path does not change group name', js: true do
+ scenario 'edit group path does not change group name', :js do
group = create(:group, :private)
visit admin_group_edit_path(group)
@@ -93,7 +93,7 @@ feature 'Admin Groups' do
end
end
- describe 'add user into a group', js: true do
+ describe 'add user into a group', :js do
shared_context 'adds user into a group' do
it do
visit admin_group_path(group)
@@ -124,7 +124,7 @@ feature 'Admin Groups' do
group.add_user(:user, Gitlab::Access::OWNER)
end
- it 'adds admin a to a group as developer', js: true do
+ it 'adds admin a to a group as developer', :js do
visit group_group_members_path(group)
page.within '.users-group-form' do
@@ -141,7 +141,7 @@ feature 'Admin Groups' do
end
end
- describe 'admin remove himself from a group', js: true do
+ describe 'admin remove himself from a group', :js do
it 'removes admin from the group' do
group.add_user(current_user, Gitlab::Access::DEVELOPER)
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index 37fd3e171eb..09e6965849a 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature "Admin Health Check", feature: true, broken_storage: true do
+feature "Admin Health Check", :feature, :broken_storage do
include StubENV
before do
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 91f08dbad5d..2e65fcc5231 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -74,7 +74,7 @@ describe 'Admin::Hooks', :js do
end
end
- describe 'Test', js: true do
+ describe 'Test', :js do
before do
WebMock.stub_request(:post, @system_hook.url)
visit admin_hooks_path
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index ae9b47299e6..a5834056a1d 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'admin issues labels' do
end
end
- it 'deletes all labels', js: true do
+ it 'deletes all labels', :js do
page.within '.labels' do
page.all('.btn-remove').each do |remove|
remove.click
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index f4f2505d436..94b12105088 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -28,7 +28,7 @@ describe "Admin::Projects" do
expect(page).not_to have_content(archived_project.name)
end
- it 'renders all projects', js: true do
+ it 'renders all projects', :js do
find(:css, '#sort-projects-dropdown').click
click_link 'Show archived projects'
@@ -37,7 +37,7 @@ describe "Admin::Projects" do
expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
end
- it 'renders only archived projects', js: true do
+ it 'renders only archived projects', :js do
find(:css, '#sort-projects-dropdown').click
click_link 'Show archived projects only'
@@ -74,7 +74,7 @@ describe "Admin::Projects" do
.to receive(:move_uploads_to_new_namespace).and_return(true)
end
- it 'transfers project to group web', js: true do
+ it 'transfers project to group web', :js do
visit admin_project_path(project)
click_button 'Search for Namespace'
@@ -91,7 +91,7 @@ describe "Admin::Projects" do
project.team << [user, :master]
end
- it 'adds admin a to a project as developer', js: true do
+ it 'adds admin a to a project as developer', :js do
visit project_project_members_path(project)
page.within '.users-project-form' do
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index 034682dae27..388d30828a7 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Admin > Users > Impersonation Tokens', js: true do
+describe 'Admin > Users > Impersonation Tokens', :js do
let(:admin) { create(:admin) }
let!(:user) { create(:user) }
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index e2e2b13cf8a..f9f4bd6f5b9 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -288,7 +288,7 @@ describe "Admin::Users" do
end
end
- it 'allows group membership to be revoked', js: true do
+ it 'allows group membership to be revoked', :js do
page.within(first('.group_member')) do
find('.btn-remove').click
end
@@ -309,7 +309,7 @@ describe "Admin::Users" do
end
end
- describe 'remove users secondary email', js: true do
+ describe 'remove users secondary email', :js do
let!(:secondary_email) do
create :email, email: 'secondary@example.com', user: user
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index c2b7543a690..42f5b5eb8dc 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -32,7 +32,7 @@ feature 'Admin uses repository checks' do
end
end
- scenario 'to clear all repository checks', js: true do
+ scenario 'to clear all repository checks', :js do
visit admin_application_settings_path
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
index dff6f96b663..4a7c3e4f1ab 100644
--- a/spec/features/auto_deploy_spec.rb
+++ b/spec/features/auto_deploy_spec.rb
@@ -31,7 +31,7 @@ describe 'Auto deploy' do
expect(page).to have_link('Set up auto deploy')
end
- it 'includes OpenShift as an available template', js: true do
+ it 'includes OpenShift as an available template', :js do
click_link 'Set up auto deploy'
click_button 'Apply a GitLab CI Yaml template'
@@ -40,7 +40,7 @@ describe 'Auto deploy' do
end
end
- it 'creates a merge request using "auto-deploy" branch', js: true do
+ it 'creates a merge request using "auto-deploy" branch', :js do
click_link 'Set up auto deploy'
click_button 'Apply a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 33aca6cb527..91c4e5037de 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Issue Boards', js: true do
+describe 'Issue Boards', :js do
include DragTo
let(:group) { create(:group, :nested) }
diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb
index 61b53aa5d1e..435de3861cf 100644
--- a/spec/features/boards/keyboard_shortcut_spec.rb
+++ b/spec/features/boards/keyboard_shortcut_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Issue Boards shortcut', js: true do
+describe 'Issue Boards shortcut', :js do
let(:project) { create(:project) }
before do
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index f67372337ec..5ac4d87e90b 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Issue Boards new issue', js: true do
+describe 'Issue Boards new issue', :js do
let(:project) { create(:project, :public) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, position: 0) }
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index c3bf50ef9d1..c4817eb947b 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Issue Boards', js: true do
+describe 'Issue Boards', :js do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb
index af4cc00162a..9accd7bb07c 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/ci_lint_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'CI Lint', js: true do
+describe 'CI Lint', :js do
before do
sign_in(create(:user))
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index 45213dc6995..d5e9de20e59 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Container Registry", js: true do
+describe "Container Registry", :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index ebcd0ba0dcd..c6ba1211b9e 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Copy as GFM', js: true do
+describe 'Copy as GFM', :js do
include MarkupHelper
include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index bfe9dac3bd4..177cd50dd72 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Cycle Analytics', js: true do
+feature 'Cycle Analytics', :js do
let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb
index 08d8cc7922b..8bab501134b 100644
--- a/spec/features/dashboard/active_tab_spec.rb
+++ b/spec/features/dashboard/active_tab_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Active Tab', js: true do
+RSpec.describe 'Dashboard Active Tab', :js do
before do
sign_in(create(:user))
end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index b6dce1b8ec4..349f9a47112 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Tooltips on .timeago dates', js: true do
+feature 'Tooltips on .timeago dates', :js do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:created_date) { Date.yesterday.to_time }
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index 60a16830cdc..1213f8c32eb 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -5,7 +5,13 @@ RSpec.describe 'Dashboard Group' do
sign_in(create(:user))
end
- it 'creates new group', js: true do
+ it 'defaults sort dropdown to last created' do
+ visit dashboard_groups_path
+
+ expect(page).to have_button('Last created')
+ end
+
+ it 'creates new group', :js do
visit dashboard_groups_path
find('.btn-new').trigger('click')
new_path = 'Samurai'
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 795335aa106..5610894fd9a 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Dashboard Issues' do
expect(page).not_to have_content(other_issue.title)
end
- it 'shows checkmark when unassigned is selected for assignee', js: true do
+ it 'shows checkmark when unassigned is selected for assignee', :js do
find('.js-assignee-search').click
find('li', text: 'Unassigned').click
find('.js-assignee-search').click
@@ -32,7 +32,7 @@ RSpec.describe 'Dashboard Issues' do
expect(find('li[data-user-id="0"] a.is-active')).to be_visible
end
- it 'shows issues when current user is author', js: true do
+ it 'shows issues when current user is author', :js do
find('#assignee_id', visible: false).set('')
find('.js-author-search', match: :first).click
@@ -70,7 +70,7 @@ RSpec.describe 'Dashboard Issues' do
end
describe 'new issue dropdown' do
- it 'shows projects only with issues feature enabled', js: true do
+ it 'shows projects only with issues feature enabled', :js do
find('.new-project-item-select-button').trigger('click')
page.within('.select2-results') do
@@ -79,7 +79,7 @@ RSpec.describe 'Dashboard Issues' do
end
end
- it 'shows the new issue page', js: true do
+ it 'shows the new issue page', :js do
find('.new-project-item-select-button').trigger('click')
wait_for_requests
diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb
index b1a207682c3..6802974c2ee 100644
--- a/spec/features/dashboard/label_filter_spec.rb
+++ b/spec/features/dashboard/label_filter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Dashboard > label filter', js: true do
+describe 'Dashboard > label filter', :js do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:project2) { create(:project, name: 'test2', path: 'test2', namespace: user.namespace) }
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 8204828b5b9..f01ba442e58 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -3,12 +3,13 @@ require 'spec_helper'
feature 'Dashboard Merge Requests' do
include FilterItemSelectHelper
include SortingHelper
+ include ProjectForksHelper
let(:current_user) { create :user }
let(:project) { create(:project) }
let(:public_project) { create(:project, :public, :repository) }
- let(:forked_project) { Projects::ForkService.new(public_project, current_user).execute }
+ let(:forked_project) { fork_project(public_project, current_user, repository: true) }
before do
project.add_master(current_user)
@@ -23,7 +24,7 @@ feature 'Dashboard Merge Requests' do
visit merge_requests_dashboard_path
end
- it 'shows projects only with merge requests feature enabled', js: true do
+ it 'shows projects only with merge requests feature enabled', :js do
find('.new-project-item-select-button').trigger('click')
page.within('.select2-results') do
@@ -88,7 +89,7 @@ feature 'Dashboard Merge Requests' do
expect(page).not_to have_content(other_merge_request.title)
end
- it 'shows authored merge requests', js: true do
+ it 'shows authored merge requests', :js do
filter_item_select('Any Assignee', '.js-assignee-search')
filter_item_select(current_user.to_reference, '.js-author-search')
@@ -100,7 +101,7 @@ feature 'Dashboard Merge Requests' do
expect(page).not_to have_content(other_merge_request.title)
end
- it 'shows all merge requests', js: true do
+ it 'shows all merge requests', :js do
filter_item_select('Any Assignee', '.js-assignee-search')
filter_item_select('Any Author', '.js-author-search')
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index 4a004107408..8f96899fb4f 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project member activity', js: true do
+feature 'Project member activity', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, name: 'x', namespace: user.namespace) }
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 4da95ccc169..fbf8b5c0db6 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -80,7 +80,7 @@ feature 'Dashboard Projects' do
end
end
- describe 'with a pipeline', clean_gitlab_redis_shared_state: true do
+ describe 'with a pipeline', :clean_gitlab_redis_shared_state do
let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
before do
diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb
index 54d477f7274..ad0f132da8c 100644
--- a/spec/features/dashboard/todos/todos_filtering_spec.rb
+++ b/spec/features/dashboard/todos/todos_filtering_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard > User filters todos', js: true do
+feature 'Dashboard > User filters todos', :js do
let(:user_1) { create(:user, username: 'user_1', name: 'user_1') }
let(:user_2) { create(:user, username: 'user_2', name: 'user_2') }
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 30bab7eeaa7..01aca443f4a 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -17,7 +17,7 @@ feature 'Dashboard Todos' do
end
end
- context 'User has a todo', js: true do
+ context 'User has a todo', :js do
before do
create(:todo, :mentioned, user: user, project: project, target: issue, author: author)
sign_in(user)
@@ -177,7 +177,7 @@ feature 'Dashboard Todos' do
end
end
- context 'User has done todos', js: true do
+ context 'User has done todos', :js do
before do
create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author)
sign_in(user)
@@ -249,7 +249,7 @@ feature 'Dashboard Todos' do
expect(page).to have_selector('.gl-pagination .page', count: 2)
end
- describe 'mark all as done', js: true do
+ describe 'mark all as done', :js do
before do
visit dashboard_todos_path
find('.js-todos-mark-all').trigger('click')
@@ -267,7 +267,7 @@ feature 'Dashboard Todos' do
end
end
- describe 'undo mark all as done', js: true do
+ describe 'undo mark all as done', :js do
before do
visit dashboard_todos_path
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 357d86497d9..1dd7547a7fc 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Expand and collapse diffs', js: true do
+feature 'Expand and collapse diffs', :js do
let(:branch) { 'expand-collapse-diffs' }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb
index e1c74a24890..c5ec495a418 100644
--- a/spec/features/explore/new_menu_spec.rb
+++ b/spec/features/explore/new_menu_spec.rb
@@ -128,12 +128,6 @@ feature 'Top Plus Menu', :js do
expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet')
end
- scenario 'public project has no New Issue Button' do
- visit project_path(public_project)
-
- hasnot_topmenuitem("New issue")
- end
-
scenario 'public project has no New merge request menu item' do
visit project_path(public_project)
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index 53b3bb3b65f..3c2186b3598 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -49,7 +49,7 @@ describe "GitLab Flavored Markdown" do
end
end
- describe "for issues", js: true do
+ describe "for issues", :js do
before do
@other_issue = create(:issue,
author: user,
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index 37814ba6238..d2d0be35f1c 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Group variables', js: true do
+feature 'Group variables', :js do
let(:user) { create(:user) }
let(:group) { create(:group) }
diff --git a/spec/features/groups/labels/subscription_spec.rb b/spec/features/groups/labels/subscription_spec.rb
index 1dd09d4f203..2e06caf98f6 100644
--- a/spec/features/groups/labels/subscription_spec.rb
+++ b/spec/features/groups/labels/subscription_spec.rb
@@ -11,7 +11,7 @@ feature 'Labels subscription' do
gitlab_sign_in user
end
- scenario 'users can subscribe/unsubscribe to group labels', js: true do
+ scenario 'users can subscribe/unsubscribe to group labels', :js do
visit group_labels_path(group)
expect(page).to have_content('feature')
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index da2edd8b980..cc8906fa969 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -85,7 +85,7 @@ feature 'Group' do
end
end
- describe 'create a nested group', :nested_groups, js: true do
+ describe 'create a nested group', :nested_groups, :js do
let(:group) { create(:group, path: 'foo') }
context 'as admin' do
@@ -131,7 +131,7 @@ feature 'Group' do
expect(page).not_to have_content('secret-group')
end
- describe 'group edit', js: true do
+ describe 'group edit', :js do
let(:group) { create(:group) }
let(:path) { edit_group_path(group) }
let(:new_name) { 'new-name' }
@@ -196,7 +196,7 @@ feature 'Group' do
end
end
- describe 'group page with nested groups', :nested_groups, js: true do
+ describe 'group page with nested groups', :nested_groups, :js do
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, namespace: group) }
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
index 925d026ed61..caee7a67aec 100644
--- a/spec/features/issuables/default_sort_order_spec.rb
+++ b/spec/features/issuables/default_sort_order_spec.rb
@@ -26,7 +26,7 @@ describe 'Projects > Issuables > Default sort order' do
MergeRequest.all
end
- context 'in the "merge requests" tab', js: true do
+ context 'in the "merge requests" tab', :js do
let(:issuable_type) { :merge_request }
it 'is "last created"' do
@@ -37,7 +37,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "merge requests / open" tab', js: true do
+ context 'in the "merge requests / open" tab', :js do
let(:issuable_type) { :merge_request }
it 'is "created date"' do
@@ -49,7 +49,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "merge requests / merged" tab', js: true do
+ context 'in the "merge requests / merged" tab', :js do
let(:issuable_type) { :merged_merge_request }
it 'is "last updated"' do
@@ -61,7 +61,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "merge requests / closed" tab', js: true do
+ context 'in the "merge requests / closed" tab', :js do
let(:issuable_type) { :closed_merge_request }
it 'is "last updated"' do
@@ -73,7 +73,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "merge requests / all" tab', js: true do
+ context 'in the "merge requests / all" tab', :js do
let(:issuable_type) { :merge_request }
it 'is "created date"' do
@@ -102,7 +102,7 @@ describe 'Projects > Issuables > Default sort order' do
Issue.all
end
- context 'in the "issues" tab', js: true do
+ context 'in the "issues" tab', :js do
let(:issuable_type) { :issue }
it 'is "created date"' do
@@ -114,7 +114,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "issues / open" tab', js: true do
+ context 'in the "issues / open" tab', :js do
let(:issuable_type) { :issue }
it 'is "created date"' do
@@ -126,7 +126,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "issues / closed" tab', js: true do
+ context 'in the "issues / closed" tab', :js do
let(:issuable_type) { :closed_issue }
it 'is "last updated"' do
@@ -138,7 +138,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "issues / all" tab', js: true do
+ context 'in the "issues / all" tab', :js do
let(:issuable_type) { :issue }
it 'is "created date"' do
diff --git a/spec/features/issuables/discussion_lock_spec.rb b/spec/features/issuables/discussion_lock_spec.rb
new file mode 100644
index 00000000000..7ea29ff252b
--- /dev/null
+++ b/spec/features/issuables/discussion_lock_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe 'Discussion Lock', :js do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project, author: user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when a user is a team member' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when the discussion is unlocked' do
+ it 'the user can lock the issue' do
+ visit project_issue_path(project, issue)
+
+ expect(find('.issuable-sidebar')).to have_content('Unlocked')
+
+ page.within('.issuable-sidebar') do
+ find('.lock-edit').click
+ click_button('Lock')
+ end
+
+ expect(find('#notes')).to have_content('locked this issue')
+ end
+ end
+
+ context 'when the discussion is locked' do
+ before do
+ issue.update_attribute(:discussion_locked, true)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'the user can unlock the issue' do
+ expect(find('.issuable-sidebar')).to have_content('Locked')
+
+ page.within('.issuable-sidebar') do
+ find('.lock-edit').click
+ click_button('Unlock')
+ end
+
+ expect(find('#notes')).to have_content('unlocked this issue')
+ expect(find('.issuable-sidebar')).to have_content('Unlocked')
+ end
+
+ it 'the user can create a comment' do
+ page.within('#notes .js-main-target-form') do
+ fill_in 'note[note]', with: 'Some new comment'
+ click_button 'Comment'
+ end
+
+ wait_for_requests
+
+ expect(find('div#notes')).to have_content('Some new comment')
+ end
+ end
+ end
+
+ context 'when a user is not a team member' do
+ context 'when the discussion is unlocked' do
+ before do
+ visit project_issue_path(project, issue)
+ end
+
+ it 'the user can not lock the issue' do
+ expect(find('.issuable-sidebar')).to have_content('Unlocked')
+ expect(find('.issuable-sidebar')).not_to have_selector('.lock-edit')
+ end
+
+ it 'the user can create a comment' do
+ page.within('#notes .js-main-target-form') do
+ fill_in 'note[note]', with: 'Some new comment'
+ click_button 'Comment'
+ end
+
+ wait_for_requests
+
+ expect(find('div#notes')).to have_content('Some new comment')
+ end
+ end
+
+ context 'when the discussion is locked' do
+ before do
+ issue.update_attribute(:discussion_locked, true)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'the user can not unlock the issue' do
+ expect(find('.issuable-sidebar')).to have_content('Locked')
+ expect(find('.issuable-sidebar')).not_to have_selector('.lock-edit')
+ end
+
+ it 'the user can not create a comment' do
+ page.within('#notes') do
+ expect(page).not_to have_selector('js-main-target-form')
+ expect(page.find('.disabled-comment'))
+ .to have_content('This issue is locked. Only project members can comment.')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/issuables/user_sees_sidebar_spec.rb b/spec/features/issuables/user_sees_sidebar_spec.rb
index 2bd1c8aab86..c6c2e58ecea 100644
--- a/spec/features/issuables/user_sees_sidebar_spec.rb
+++ b/spec/features/issuables/user_sees_sidebar_spec.rb
@@ -12,7 +12,7 @@ describe 'Issue Sidebar on Mobile' do
sign_in(user)
end
- context 'mobile sidebar on merge requests', js: true do
+ context 'mobile sidebar on merge requests', :js do
before do
visit project_merge_request_path(merge_request.project, merge_request)
end
@@ -20,7 +20,7 @@ describe 'Issue Sidebar on Mobile' do
it_behaves_like "issue sidebar stays collapsed on mobile"
end
- context 'mobile sidebar on issues', js: true do
+ context 'mobile sidebar on issues', :js do
before do
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index a29acb30163..850b35c4467 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -24,7 +24,7 @@ describe 'Awards Emoji' do
end
# Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
- it 'does not shows a 500 page', js: true do
+ it 'does not shows a 500 page', :js do
expect(page).to have_text(issue.title)
end
end
@@ -37,37 +37,37 @@ describe 'Awards Emoji' do
wait_for_requests
end
- it 'increments the thumbsdown emoji', js: true do
+ it 'increments the thumbsdown emoji', :js do
find('[data-name="thumbsdown"]').click
wait_for_requests
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
- it 'increments the thumbsup emoji', js: true do
+ it 'increments the thumbsup emoji', :js do
find('[data-name="thumbsup"]').click
wait_for_requests
expect(thumbsup_emoji).to have_text("1")
end
- it 'decrements the thumbsdown emoji', js: true do
+ it 'decrements the thumbsdown emoji', :js do
expect(thumbsdown_emoji).to have_text("0")
end
end
context 'click the thumbsdown emoji' do
- it 'increments the thumbsdown emoji', js: true do
+ it 'increments the thumbsdown emoji', :js do
find('[data-name="thumbsdown"]').click
wait_for_requests
expect(thumbsdown_emoji).to have_text("1")
end
- it 'decrements the thumbsup emoji', js: true do
+ it 'decrements the thumbsup emoji', :js do
expect(thumbsup_emoji).to have_text("0")
end
end
- it 'toggles the smiley emoji on a note', js: true do
+ it 'toggles the smiley emoji on a note', :js do
toggle_smiley_emoji(true)
within('.note-body') do
@@ -82,7 +82,7 @@ describe 'Awards Emoji' do
end
context 'execute /award quick action' do
- it 'toggles the emoji award on noteable', js: true do
+ it 'toggles the emoji award on noteable', :js do
execute_quick_action('/award :100:')
expect(find(noteable_award_counter)).to have_text("1")
@@ -95,7 +95,7 @@ describe 'Awards Emoji' do
end
end
- context 'unauthorized user', js: true do
+ context 'unauthorized user', :js do
before do
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb
index e95eb19f7d1..ddb69d414da 100644
--- a/spec/features/issues/award_spec.rb
+++ b/spec/features/issues/award_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issue awards', js: true do
+feature 'Issue awards', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index a89dcdf41dc..3223eb20b55 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -9,7 +9,7 @@ feature 'Issues > Labels bulk assignment' do
let!(:feature) { create(:label, project: project, title: 'feature') }
let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
- context 'as an allowed user', js: true do
+ context 'as an allowed user', :js do
before do
project.team << [user, :master]
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 80cc8d22999..822ba48e005 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Resolving all open discussions in a merge request from an issue', js: true do
+feature 'Resolving all open discussions in a merge request from an issue', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
index ad5fd0fd97b..f0bed85595c 100644
--- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
@@ -24,7 +24,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do
end
end
- context 'resolving the discussion', js: true do
+ context 'resolving the discussion', :js do
before do
click_button 'Resolve discussion'
end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 3cec59050ab..5e20fb48768 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Dropdown author', js: true do
+describe 'Dropdown author', :js do
include FilteredSearchHelpers
let!(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index 44741bcc92d..3012c77f2b9 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Dropdown emoji', js: true do
+describe 'Dropdown emoji', :js do
include FilteredSearchHelpers
let!(:project) { create(:project, :public) }
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index c46803112a9..cbc4f8d4c50 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Dropdown label', js: true do
+describe 'Dropdown label', :js do
include FilteredSearchHelpers
let(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 630d6a10c9c..2974016c6a7 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Filter issues', js: true do
+describe 'Filter issues', :js do
include FilteredSearchHelpers
let(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index 447281ed19d..eef7988e2bd 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Recent searches', js: true do
+describe 'Recent searches', :js do
include FilteredSearchHelpers
let(:project_1) { create(:project, :public) }
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index d4dd570fb37..88688422dc7 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Search bar', js: true do
+describe 'Search bar', :js do
include FilteredSearchHelpers
let!(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 2b624f4842d..920f5546eef 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Visual tokens', js: true do
+describe 'Visual tokens', :js do
include FilteredSearchHelpers
include WaitForRequests
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index c6cf6265645..15041ff04ea 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'GFM autocomplete', js: true do
+feature 'GFM autocomplete', :js do
let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') }
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 28b636f9359..c0c396af93f 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -28,8 +28,7 @@ feature 'Issue Detail', :js do
fill_in 'issue-title', with: 'issue title'
click_button 'Save'
- visit profile_account_path
- click_link 'Delete account'
+ Users::DestroyService.new(user).execute(user)
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index af11b474842..bc9c3d825c1 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -13,7 +13,7 @@ feature 'Issue Sidebar' do
sign_in(user)
end
- context 'assignee', js: true do
+ context 'assignee', :js do
let(:user2) { create(:user) }
let(:issue2) { create(:issue, project: project, author: user2) }
@@ -82,7 +82,7 @@ feature 'Issue Sidebar' do
visit_issue(project, issue)
end
- context 'sidebar', js: true do
+ context 'sidebar', :js do
it 'changes size when the screen size is smaller' do
sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
# Resize the window
@@ -101,7 +101,7 @@ feature 'Issue Sidebar' do
end
end
- context 'editing issue labels', js: true do
+ context 'editing issue labels', :js do
before do
page.within('.block.labels') do
find('.edit-link').click
@@ -114,7 +114,7 @@ feature 'Issue Sidebar' do
end
end
- context 'creating a new label', js: true do
+ context 'creating a new label', :js do
before do
page.within('.block.labels') do
click_link 'Create new'
diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb
index 634ea111dc1..6869c2c869d 100644
--- a/spec/features/issues/markdown_toolbar_spec.rb
+++ b/spec/features/issues/markdown_toolbar_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issue markdown toolbar', js: true do
+feature 'Issue markdown toolbar', :js do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index b2724945da4..6d7b1b1cd8f 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -37,7 +37,7 @@ feature 'issue move to another project' do
visit issue_path(issue)
end
- scenario 'moving issue to another project', js: true do
+ scenario 'moving issue to another project', :js do
find('.js-move-issue').trigger('click')
wait_for_requests
all('.js-move-issue-dropdown-item')[0].click
@@ -49,7 +49,7 @@ feature 'issue move to another project' do
expect(page.current_path).to include project_path(new_project)
end
- scenario 'searching project dropdown', js: true do
+ scenario 'searching project dropdown', :js do
new_project_search.team << [user, :reporter]
find('.js-move-issue').trigger('click')
@@ -63,7 +63,7 @@ feature 'issue move to another project' do
end
end
- context 'user does not have permission to move the issue to a project', js: true do
+ context 'user does not have permission to move the issue to a project', :js do
let!(:private_project) { create(:project, :private) }
let(:another_project) { create(:project) }
background { another_project.team << [user, :guest] }
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index 332ce78b138..d25231d624c 100644
--- a/spec/features/issues/spam_issues_spec.rb
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'New issue', js: true do
+describe 'New issue', :js do
include StubENV
let(:project) { create(:project, :public) }
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 8405f1cd48d..29a2d38ae18 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Manually create a todo item from issue', js: true do
+feature 'Manually create a todo item from issue', :js do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 7437c469a72..9f5e25ff2cb 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issues > User uses quick actions', js: true do
+feature 'Issues > User uses quick actions', :js do
include QuickActionsHelpers
it_behaves_like 'issuable record that supports quick actions in its description and notes', :issue do
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index aa8cf3b013c..25e99774575 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -270,7 +270,7 @@ describe 'Issues', :js do
visit namespace_project_issues_path(user.namespace, project1)
end
- it 'changes incoming email address token', js: true do
+ it 'changes incoming email address token', :js do
find('.issue-email-modal-btn').click
previous_token = find('input#issue_email').value
find('.incoming-email-token-reset').trigger('click')
@@ -286,7 +286,7 @@ describe 'Issues', :js do
end
end
- describe 'update labels from issue#show', js: true do
+ describe 'update labels from issue#show', :js do
let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
let!(:label) { create(:label, project: project) }
@@ -309,7 +309,7 @@ describe 'Issues', :js do
let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
context 'by authorized user' do
- it 'allows user to select unassigned', js: true do
+ it 'allows user to select unassigned', :js do
visit project_issue_path(project, issue)
page.within('.assignee') do
@@ -327,7 +327,7 @@ describe 'Issues', :js do
expect(issue.reload.assignees).to be_empty
end
- it 'allows user to select an assignee', js: true do
+ it 'allows user to select an assignee', :js do
issue2 = create(:issue, project: project, author: user)
visit project_issue_path(project, issue2)
@@ -348,7 +348,7 @@ describe 'Issues', :js do
end
end
- it 'allows user to unselect themselves', js: true do
+ it 'allows user to unselect themselves', :js do
issue2 = create(:issue, project: project, author: user)
visit project_issue_path(project, issue2)
@@ -377,7 +377,7 @@ describe 'Issues', :js do
project.team << [[guest], :guest]
end
- it 'shows assignee text', js: true do
+ it 'shows assignee text', :js do
sign_out(:user)
sign_in(guest)
@@ -392,7 +392,7 @@ describe 'Issues', :js do
let!(:milestone) { create(:milestone, project: project) }
context 'by authorized user' do
- it 'allows user to select unassigned', js: true do
+ it 'allows user to select unassigned', :js do
visit project_issue_path(project, issue)
page.within('.milestone') do
@@ -410,7 +410,7 @@ describe 'Issues', :js do
expect(issue.reload.milestone).to be_nil
end
- it 'allows user to de-select milestone', js: true do
+ it 'allows user to de-select milestone', :js do
visit project_issue_path(project, issue)
page.within('.milestone') do
@@ -440,7 +440,7 @@ describe 'Issues', :js do
issue.save
end
- it 'shows milestone text', js: true do
+ it 'shows milestone text', :js do
sign_out(:user)
sign_in(guest)
@@ -473,7 +473,7 @@ describe 'Issues', :js do
end
end
- context 'dropzone upload file', js: true do
+ context 'dropzone upload file', :js do
before do
visit new_project_issue_path(project)
end
@@ -544,7 +544,7 @@ describe 'Issues', :js do
end
describe 'due date' do
- context 'update due on issue#show', js: true do
+ context 'update due on issue#show', :js do
let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
before do
@@ -588,8 +588,8 @@ describe 'Issues', :js do
end
end
- describe 'title issue#show', js: true do
- it 'updates the title', js: true do
+ describe 'title issue#show', :js do
+ it 'updates the title', :js do
issue = create(:issue, author: user, assignees: [user], project: project, title: 'new title')
visit project_issue_path(project, issue)
@@ -603,20 +603,20 @@ describe 'Issues', :js do
end
end
- describe 'confidential issue#show', js: true do
+ describe 'confidential issue#show', :js do
it 'shows confidential sibebar information as confidential and can be turned off' do
issue = create(:issue, :confidential, project: project)
visit project_issue_path(project, issue)
- expect(page).to have_css('.confidential-issue-warning')
- expect(page).to have_css('.is-confidential')
- expect(page).not_to have_css('.is-not-confidential')
+ expect(page).to have_css('.issuable-note-warning')
+ expect(find('.issuable-sidebar-item.confidentiality')).to have_css('.is-active')
+ expect(find('.issuable-sidebar-item.confidentiality')).not_to have_css('.not-active')
find('.confidential-edit').click
- expect(page).to have_css('.confidential-warning-message')
+ expect(page).to have_css('.sidebar-item-warning-message')
- within('.confidential-warning-message') do
+ within('.sidebar-item-warning-message') do
find('.btn-close').click
end
@@ -624,7 +624,7 @@ describe 'Issues', :js do
visit project_issue_path(project, issue)
- expect(page).not_to have_css('.is-confidential')
+ expect(page).not_to have_css('.is-active')
end
end
end
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index c9983f0941f..6dfabcc7225 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -197,7 +197,7 @@ feature 'Login' do
expect(page).to have_content('The global settings require you to enable Two-Factor Authentication for your account. You need to do this before ')
end
- it 'allows skipping two-factor configuration', js: true do
+ it 'allows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later'
@@ -215,7 +215,7 @@ feature 'Login' do
)
end
- it 'disallows skipping two-factor configuration', js: true do
+ it 'disallows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
@@ -260,7 +260,7 @@ feature 'Login' do
'before ')
end
- it 'allows skipping two-factor configuration', js: true do
+ it 'allows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later'
@@ -279,7 +279,7 @@ feature 'Login' do
)
end
- it 'disallows skipping two-factor configuration', js: true do
+ it 'disallows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb
index 63fa72650ac..d49d145f254 100644
--- a/spec/features/merge_requests/assign_issues_spec.rb
+++ b/spec/features/merge_requests/assign_issues_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Merge request issue assignment', js: true do
+feature 'Merge request issue assignment', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue1) { create(:issue, project: project) }
diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb
index e886309133d..a24464f2556 100644
--- a/spec/features/merge_requests/award_spec.rb
+++ b/spec/features/merge_requests/award_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Merge request awards', js: true do
+feature 'Merge request awards', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
index 1f5e7b55fb0..fbbfe7942be 100644
--- a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
+++ b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Check if mergeable with unresolved discussions', js: true do
+feature 'Check if mergeable with unresolved discussions', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb
index 4b1e1b9a8d4..48f370c3ad4 100644
--- a/spec/features/merge_requests/cherry_pick_spec.rb
+++ b/spec/features/merge_requests/cherry_pick_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Cherry-pick Merge Requests', js: true do
+describe 'Cherry-pick Merge Requests', :js do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: group) }
diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
index 299b4f5708a..4dd4e40f52c 100644
--- a/spec/features/merge_requests/closes_issues_spec.rb
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge Request closing issues message', js: true do
+feature 'Merge Request closing issues message', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue_1) { create(:issue, project: project)}
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 2d2c674f8fb..b0432ed8fc6 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge request conflict resolution', js: true do
+feature 'Merge request conflict resolution', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/merge_requests/create_new_mr_from_fork_spec.rb b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb
new file mode 100644
index 00000000000..93c40ff6443
--- /dev/null
+++ b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+feature 'Creating a merge request from a fork', :js do
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let!(:source_project) do
+ fork_project(project, user,
+ repository: true,
+ namespace: user.namespace)
+ end
+
+ before do
+ source_project.add_master(user)
+
+ sign_in(user)
+ end
+
+ shared_examples 'create merge request to other project' do
+ it 'has all possible target projects' do
+ visit project_new_merge_request_path(source_project)
+
+ first('.js-target-project').click
+
+ within('.dropdown-target-project .dropdown-content') do
+ expect(page).to have_content(project.full_path)
+ expect(page).to have_content(target_project.full_path)
+ expect(page).to have_content(source_project.full_path)
+ end
+ end
+
+ it 'allows creating the merge request to another target project' do
+ visit project_merge_requests_path(source_project)
+
+ page.within '.content' do
+ click_link 'New merge request'
+ end
+
+ find('.js-source-branch', match: :first).click
+ find('.dropdown-source-branch .dropdown-content a', match: :first).click
+
+ first('.js-target-project').click
+ find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click
+
+ click_button 'Compare branches and continue'
+
+ wait_for_requests
+
+ expect { click_button 'Submit merge request' }
+ .to change { target_project.merge_requests.reload.size }.by(1)
+ end
+
+ it 'updates the branches when selecting a new target project' do
+ target_project_member = target_project.owner
+ CreateBranchService.new(target_project, target_project_member)
+ .execute('a-brand-new-branch-to-test', 'master')
+ visit project_new_merge_request_path(source_project)
+
+ first('.js-target-project').click
+ find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click
+
+ wait_for_requests
+
+ first('.js-target-branch').click
+
+ within('.dropdown-target-branch .dropdown-content') do
+ expect(page).to have_content('a-brand-new-branch-to-test')
+ end
+ end
+ end
+
+ context 'creating to the source of a fork' do
+ let!(:target_project) { project }
+
+ it_behaves_like('create merge request to other project')
+ end
+
+ context 'creating to a sibling of a fork' do
+ let!(:target_project) do
+ other_user = create(:user)
+ fork_project(project, other_user,
+ repository: true,
+ namespace: other_user.namespace)
+ end
+
+ it_behaves_like('create merge request to other project')
+ end
+end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index 96e8027a54d..5402d61da54 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Create New Merge Request', js: true do
+feature 'Create New Merge Request', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 09541873f71..d03ddfece74 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -1,21 +1,20 @@
require 'spec_helper'
feature 'Merge request created from fork' do
+ include ProjectForksHelper
+
given(:user) { create(:user) }
given(:project) { create(:project, :public, :repository) }
- given(:fork_project) { create(:project, :public, :repository) }
+ given(:forked_project) { fork_project(project, user, repository: true) }
given!(:merge_request) do
- create(:forked_project_link, forked_to_project: fork_project,
- forked_from_project: project)
-
- create(:merge_request_with_diffs, source_project: fork_project,
+ create(:merge_request_with_diffs, source_project: forked_project,
target_project: project,
description: 'Test merge request')
end
background do
- fork_project.team << [user, :master]
+ forked_project.team << [user, :master]
sign_in user
end
@@ -31,11 +30,11 @@ feature 'Merge request created from fork' do
background do
create(:note_on_commit, note: comment,
- project: fork_project,
+ project: forked_project,
commit_id: merge_request.commit_shas.first)
end
- scenario 'user can reply to the comment', js: true do
+ scenario 'user can reply to the comment', :js do
visit_merge_request(merge_request)
expect(page).to have_content(comment)
@@ -55,10 +54,10 @@ feature 'Merge request created from fork' do
context 'source project is deleted' do
background do
MergeRequests::MergeService.new(project, user).execute(merge_request)
- fork_project.destroy!
+ forked_project.destroy!
end
- scenario 'user can access merge request', js: true do
+ scenario 'user can access merge request', :js do
visit_merge_request(merge_request)
expect(page).to have_content 'Test merge request'
@@ -69,7 +68,7 @@ feature 'Merge request created from fork' do
context 'pipeline present in source project' do
given(:pipeline) do
create(:ci_pipeline,
- project: fork_project,
+ project: forked_project,
sha: merge_request.diff_head_sha,
ref: merge_request.source_branch)
end
@@ -79,7 +78,7 @@ feature 'Merge request created from fork' do
create(:ci_build, pipeline: pipeline, name: 'spinach')
end
- scenario 'user visits a pipelines page', js: true do
+ scenario 'user visits a pipelines page', :js do
visit_merge_request(merge_request)
page.within('.merge-request-tabs') { click_link 'Pipelines' }
diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb
index 874c6e2ff69..7f69e82af4c 100644
--- a/spec/features/merge_requests/deleted_source_branch_spec.rb
+++ b/spec/features/merge_requests/deleted_source_branch_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
# This test serves as a regression test for a bug that caused an error
# message to be shown by JavaScript when the source branch was deleted.
# Please do not remove "js: true".
-describe 'Deleted source branch', js: true do
+describe 'Deleted source branch', :js do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index 4766cdf716f..9aa0672feae 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Diff note avatars', js: true do
+feature 'Diff note avatars', :js do
include NoteInteractionHelpers
let(:user) { create(:user) }
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index fd110e68e84..637e6036384 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Diff notes resolve', js: true do
+feature 'Diff notes resolve', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index e9068f722d5..2adca58620f 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
-feature 'Diffs URL', js: true do
+feature 'Diffs URL', :js do
+ include ProjectForksHelper
+
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -64,7 +66,7 @@ feature 'Diffs URL', js: true do
context 'when editing file' do
let(:author_user) { create(:user) }
let(:user) { create(:user) }
- let(:forked_project) { Projects::ForkService.new(project, author_user).execute }
+ let(:forked_project) { fork_project(project, author_user, repository: true) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, target_project: project, author: author_user) }
let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") }
diff --git a/spec/features/merge_requests/discussion_lock_spec.rb b/spec/features/merge_requests/discussion_lock_spec.rb
new file mode 100644
index 00000000000..7bbd3b1e69e
--- /dev/null
+++ b/spec/features/merge_requests/discussion_lock_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe 'Discussion Lock', :js do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: user) }
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when the discussion is locked' do
+ before do
+ merge_request.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when a user is a team member' do
+ before do
+ project.add_developer(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'the user can create a comment' do
+ page.within('.issuable-discussion #notes .js-main-target-form') do
+ fill_in 'note[note]', with: 'Some new comment'
+ click_button 'Comment'
+ end
+
+ wait_for_requests
+
+ expect(find('.issuable-discussion #notes')).to have_content('Some new comment')
+ end
+ end
+
+ context 'when a user is not a team member' do
+ before do
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'the user can not create a comment' do
+ page.within('.issuable-discussion #notes') do
+ expect(page).not_to have_selector('js-main-target-form')
+ expect(page.find('.disabled-comment'))
+ .to have_content('This merge request is locked. Only project members can comment.')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index 7386e78fb13..4538555c168 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -29,7 +29,7 @@ feature 'Edit Merge Request' do
expect(page).to have_content 'Someone edited the merge request the same time you did'
end
- it 'allows to unselect "Remove source branch"', js: true do
+ it 'allows to unselect "Remove source branch"', :js do
merge_request.update(merge_params: { 'force_remove_source_branch' => '1' })
expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy
@@ -42,7 +42,7 @@ feature 'Edit Merge Request' do
expect(page).to have_content 'Remove source branch'
end
- it 'should preserve description textarea height', js: true do
+ it 'should preserve description textarea height', :js do
long_description = %q(
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat.
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index 166c02a7a7f..8b9ff9be943 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -18,7 +18,7 @@ feature 'Merge Request filtering by Milestone' do
sign_in(user)
end
- scenario 'filters by no Milestone', js: true do
+ scenario 'filters by no Milestone', :js do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
@@ -32,7 +32,7 @@ feature 'Merge Request filtering by Milestone' do
expect(page).to have_css('.merge-request', count: 1)
end
- context 'filters by upcoming milestone', js: true do
+ context 'filters by upcoming milestone', :js do
it 'does not show merge requests with no expiry' do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
@@ -67,7 +67,7 @@ feature 'Merge Request filtering by Milestone' do
end
end
- scenario 'filters by a specific Milestone', js: true do
+ scenario 'filters by a specific Milestone', :js do
create(:merge_request, :with_diffs, source_project: project, milestone: milestone)
create(:merge_request, :simple, source_project: project)
@@ -83,7 +83,7 @@ feature 'Merge Request filtering by Milestone' do
milestone.update(name: "rock 'n' roll")
end
- scenario 'filters by a specific Milestone', js: true do
+ scenario 'filters by a specific Milestone', :js do
create(:merge_request, :with_diffs, source_project: project, milestone: milestone)
create(:merge_request, :simple, source_project: project)
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index 16703bc1c01..aac295ab940 100644
--- a/spec/features/merge_requests/filter_merge_requests_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -36,7 +36,7 @@ describe 'Filter merge requests' do
expect_mr_list_count(0)
end
- context 'assignee', js: true do
+ context 'assignee', :js do
it 'updates to current user' do
expect_assignee_visual_tokens()
end
@@ -69,7 +69,7 @@ describe 'Filter merge requests' do
expect_mr_list_count(0)
end
- context 'milestone', js: true do
+ context 'milestone', :js do
it 'updates to current milestone' do
expect_milestone_visual_tokens()
end
@@ -88,7 +88,7 @@ describe 'Filter merge requests' do
end
end
- describe 'for label from mr#index', js: true do
+ describe 'for label from mr#index', :js do
it 'filters by no label' do
input_filtered_search('label:none')
@@ -137,7 +137,7 @@ describe 'Filter merge requests' do
expect_mr_list_count(0)
end
- context 'assignee and label', js: true do
+ context 'assignee and label', :js do
def expect_assignee_label_visual_tokens
wait_for_requests
@@ -183,7 +183,7 @@ describe 'Filter merge requests' do
visit project_merge_requests_path(project)
end
- context 'only text', js: true do
+ context 'only text', :js do
it 'filters merge requests by searched text' do
input_filtered_search('bug')
@@ -199,7 +199,7 @@ describe 'Filter merge requests' do
end
end
- context 'filters and searches', js: true do
+ context 'filters and searches', :js do
it 'filters by text and label' do
input_filtered_search('Bug')
@@ -289,7 +289,7 @@ describe 'Filter merge requests' do
end
end
- describe 'filter by assignee id', js: true do
+ describe 'filter by assignee id', :js do
it 'filter by current user' do
visit project_merge_requests_path(project, assignee_id: user.id)
@@ -312,7 +312,7 @@ describe 'Filter merge requests' do
end
end
- describe 'filter by author id', js: true do
+ describe 'filter by author id', :js do
it 'filter by current user' do
visit project_merge_requests_path(project, author_id: user.id)
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index de98b147d04..758fc9b139d 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -1,8 +1,10 @@
require 'rails_helper'
describe 'New/edit merge request', :js do
+ include ProjectForksHelper
+
let!(:project) { create(:project, :public, :repository) }
- let(:fork_project) { create(:project, :repository, forked_from_project: project) }
+ let(:forked_project) { fork_project(project, nil, repository: true) }
let!(:user) { create(:user) }
let!(:user2) { create(:user) }
let!(:milestone) { create(:milestone, project: project) }
@@ -170,16 +172,16 @@ describe 'New/edit merge request', :js do
context 'forked project' do
before do
- fork_project.team << [user, :master]
+ forked_project.team << [user, :master]
sign_in(user)
end
context 'new merge request' do
before do
visit project_new_merge_request_path(
- fork_project,
+ forked_project,
merge_request: {
- source_project_id: fork_project.id,
+ source_project_id: forked_project.id,
target_project_id: project.id,
source_branch: 'fix',
target_branch: 'master'
@@ -238,7 +240,7 @@ describe 'New/edit merge request', :js do
context 'edit merge request' do
before do
merge_request = create(:merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project,
source_branch: 'fix',
target_branch: 'master'
diff --git a/spec/features/merge_requests/image_diff_notes.rb b/spec/features/merge_requests/image_diff_notes.rb
new file mode 100644
index 00000000000..3c53b51e330
--- /dev/null
+++ b/spec/features/merge_requests/image_diff_notes.rb
@@ -0,0 +1,196 @@
+require 'spec_helper'
+
+feature 'image diff notes', :js do
+ include NoteInteractionHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ project.team << [user, :master]
+ sign_in user
+
+ page.driver.set_cookie('sidebar_collapsed', 'true')
+
+ # Stub helper to return any blob file as image from public app folder.
+ # This is necessary to run this specs since we don't display repo images in capybara.
+ allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_path).and_return('/apple-touch-icon.png')
+ end
+
+ context 'create commit diff notes' do
+ commit_id = '2f63565e7aac07bcdadb654e253078b727143ec4'
+
+ describe 'create a new diff note' do
+ before do
+ visit project_commit_path(project, commit_id)
+ create_image_diff_note
+ end
+
+ it 'shows indicator badge on image diff' do
+ indicator = find('.js-image-badge')
+
+ expect(indicator).to have_content('1')
+ end
+
+ it 'shows the avatar badge on the new note' do
+ badge = find('.image-diff-avatar-link .badge')
+
+ expect(badge).to have_content('1')
+ end
+
+ it 'allows collapsing/expanding the discussion notes' do
+ find('.js-diff-notes-toggle', :first).click
+
+ expect(page).not_to have_content('image diff test comment')
+
+ find('.js-diff-notes-toggle').click
+
+ expect(page).to have_content('image diff test comment')
+ end
+ end
+
+ describe 'render commit diff notes' do
+ let(:path) { "files/images/6049019_460s.jpg" }
+ let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+
+ let(:note1_position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ width: 100,
+ height: 100,
+ x: 10,
+ y: 10,
+ position_type: "image",
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ let(:note2_position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ width: 100,
+ height: 100,
+ x: 20,
+ y: 20,
+ position_type: "image",
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ let!(:note1) { create(:diff_note_on_commit, commit_id: commit.id, project: project, position: note1_position, note: 'my note 1') }
+ let!(:note2) { create(:diff_note_on_commit, commit_id: commit.id, project: project, position: note2_position, note: 'my note 2') }
+
+ before do
+ visit project_commit_path(project, commit.id)
+ wait_for_requests
+ end
+
+ it 'render diff indicators within the image diff frame' do
+ expect(page).to have_css('.js-image-badge', count: 2)
+ end
+
+ it 'shows the diff notes' do
+ expect(page).to have_css('.diff-content .note', count: 2)
+ end
+
+ it 'shows the diff notes with correct avatar badge numbers' do
+ expect(page).to have_css('.image-diff-avatar-link', text: 1)
+ expect(page).to have_css('.image-diff-avatar-link', text: 2)
+ end
+ end
+ end
+
+ %w(inline parallel).each do |view|
+ context "#{view} view" do
+ let(:merge_request) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, author: user) }
+ let(:path) { "files/images/ee_repo_logo.png" }
+
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 1,
+ position_type: "image",
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) }
+
+ describe 'creating a new diff note' do
+ before do
+ visit diffs_project_merge_request_path(project, merge_request, view: view)
+ create_image_diff_note
+ end
+
+ it 'shows indicator badge on image diff' do
+ indicator = find('.js-image-badge', match: :first)
+
+ expect(indicator).to have_content('1')
+ end
+
+ it 'shows the avatar badge on the new note' do
+ badge = find('.image-diff-avatar-link .badge', match: :first)
+
+ expect(badge).to have_content('1')
+ end
+
+ it 'allows expanding/collapsing the discussion notes' do
+ page.all('.js-diff-notes-toggle')[0].trigger('click')
+ page.all('.js-diff-notes-toggle')[1].trigger('click')
+
+ expect(page).not_to have_content('image diff test comment')
+
+ page.all('.js-diff-notes-toggle')[0].trigger('click')
+ page.all('.js-diff-notes-toggle')[1].trigger('click')
+
+ expect(page).to have_content('image diff test comment')
+ end
+ end
+ end
+ end
+
+ describe 'discussion tab polling', :js do
+ let(:merge_request) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, author: user) }
+ let(:path) { "files/images/ee_repo_logo.png" }
+
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ width: 100,
+ height: 100,
+ x: 50,
+ y: 50,
+ position_type: "image",
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ before do
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'render diff indicators within the image frame' do
+ diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+
+ wait_for_requests
+
+ expect(page).to have_selector('.image-comment-badge')
+ expect(page).to have_content(diff_note.note)
+ end
+ end
+end
+
+def create_image_diff_note
+ find('.js-add-image-diff-note-button', match: :first).click
+ page.all('.js-add-image-diff-note-button')[0].trigger('click')
+ find('.diff-content .note-textarea').native.send_keys('image diff test comment')
+ click_button 'Comment'
+ wait_for_requests
+end
diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
index 08a3bb84aac..82b2b56ef80 100644
--- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
+++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Clicking toggle commit message link', js: true do
+feature 'Clicking toggle commit message link', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue_1) { create(:issue, project: project)}
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
index 59e67420333..91f207bd339 100644
--- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Only allow merge requests to be merged if the pipeline succeeds', js: true do
+feature 'Only allow merge requests to be merged if the pipeline succeeds', :js do
let(:merge_request) { create(:merge_request_with_diffs) }
let(:project) { merge_request.target_project }
@@ -10,7 +10,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t
project.team << [merge_request.author, :master]
end
- context 'project does not have CI enabled', js: true do
+ context 'project does not have CI enabled', :js do
it 'allows MR to be merged' do
visit_merge_request(merge_request)
@@ -20,7 +20,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t
end
end
- context 'when project has CI enabled', js: true do
+ context 'when project has CI enabled', :js do
given!(:pipeline) do
create(:ci_empty_pipeline,
project: project,
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
index 347ce788b36..a3fcc27cab0 100644
--- a/spec/features/merge_requests/pipelines_spec.rb
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Pipelines for Merge Requests', js: true do
+feature 'Pipelines for Merge Requests', :js do
describe 'pipeline tab' do
given(:user) { create(:user) }
given(:merge_request) { create(:merge_request) }
diff --git a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb
index 55a82bdf2b9..25abbb469ab 100644
--- a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb
+++ b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Resolve outdated diff discussions', js: true do
+feature 'Resolve outdated diff discussions', :js do
let(:project) { create(:project, :repository, :public) }
let(:merge_request) do
diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb
index 9bbf2610bcb..bce36e05e57 100644
--- a/spec/features/merge_requests/target_branch_spec.rb
+++ b/spec/features/merge_requests/target_branch_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Target branch', js: true do
+describe 'Target branch', :js do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
diff --git a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb
index dd989fd49b2..fa3d988b27a 100644
--- a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb
+++ b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Toggle Whitespace Changes', js: true do
+feature 'Toggle Whitespace Changes', :js do
before do
sign_in(create(:admin))
merge_request = create(:merge_request)
diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb
index 4e5ec9fbd2d..cd92ad22267 100644
--- a/spec/features/merge_requests/toggler_behavior_spec.rb
+++ b/spec/features/merge_requests/toggler_behavior_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'toggler_behavior', js: true do
+feature 'toggler_behavior', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, author: user) }
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
index 9cb8a357309..1a41fd36a4f 100644
--- a/spec/features/merge_requests/update_merge_requests_spec.rb
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -10,7 +10,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
sign_in(user)
end
- context 'status', js: true do
+ context 'status', :js do
describe 'close merge request' do
before do
visit project_merge_requests_path(project)
@@ -37,7 +37,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
end
- context 'assignee', js: true do
+ context 'assignee', :js do
describe 'set assignee' do
before do
visit project_merge_requests_path(project)
@@ -67,7 +67,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
end
- context 'milestone', js: true do
+ context 'milestone', :js do
let(:milestone) { create(:milestone, project: project) }
describe 'set milestone' do
diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
index 2fb6d0b965f..7a773fb2baa 100644
--- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Merge requests > User posts diff notes', :js do
+ include MergeRequestDiffHelpers
+
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.source_project }
@@ -225,6 +227,7 @@ feature 'Merge requests > User posts diff notes', :js do
write_comment_on_line(line_holder, diff_side)
click_button 'Comment'
+
wait_for_requests
assert_comment_persistence(line_holder, asset_form_reset: asset_form_reset)
@@ -244,36 +247,6 @@ feature 'Merge requests > User posts diff notes', :js do
expect(line[:num]).not_to have_css comment_button_class
end
- def get_line_components(line_holder, diff_side = nil)
- if diff_side.nil?
- get_inline_line_components(line_holder)
- else
- get_parallel_line_components(line_holder, diff_side)
- end
- end
-
- def get_inline_line_components(line_holder)
- { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
- end
-
- def get_parallel_line_components(line_holder, diff_side = nil)
- side_index = diff_side == 'left' ? 0 : 1
- # Wait for `.line_content`
- line_holder.find('.line_content', match: :first)
- # Wait for `.diff-line-num`
- line_holder.find('.diff-line-num', match: :first)
- { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
- end
-
- def click_diff_line(line_holder, diff_side = nil)
- line = get_line_components(line_holder, diff_side)
- line[:content].hover
-
- expect(line[:num]).to have_css comment_button_class
-
- line[:num].find(comment_button_class).trigger 'click'
- end
-
def write_comment_on_line(line_holder, diff_side)
click_diff_line(line_holder, diff_side)
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index 95c50df1896..ee0766f1192 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Merge Requests > User uses quick actions', js: true do
+feature 'Merge Requests > User uses quick actions', :js do
include QuickActionsHelpers
it_behaves_like 'issuable record that supports quick actions in its description and notes', :merge_request do
diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 8e231fbc281..50f7d721ff3 100644
--- a/spec/features/merge_requests/versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge Request versions', js: true do
+feature 'Merge Request versions', :js do
let(:merge_request) { create(:merge_request, importing: true) }
let(:project) { merge_request.source_project }
let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb
index c0221525c9f..5658c2c5122 100644
--- a/spec/features/merge_requests/widget_deployments_spec.rb
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Widget Deployments Header', js: true do
+feature 'Widget Deployments Header', :js do
describe 'when deployed to an environment' do
given(:user) { create(:user) }
given(:project) { merge_request.target_project }
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index 791cfa308c3..2bad3b02250 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -3,10 +3,13 @@ require 'rails_helper'
describe 'Merge request', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
+ let(:project_only_mwps) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true) }
let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request_in_only_mwps_project) { create(:merge_request, source_project: project_only_mwps) }
before do
- project.team << [user, :master]
+ project.add_master(user)
+ project_only_mwps.add_master(user)
sign_in(user)
end
@@ -160,6 +163,20 @@ describe 'Merge request', :js do
end
end
+ context 'view merge request in project with only-mwps setting enabled but no CI is setup' do
+ before do
+ visit project_merge_request_path(project_only_mwps, merge_request_in_only_mwps_project)
+ end
+
+ it 'should be allowed to merge' do
+ # Wait for the `ci_status` and `merge_check` requests
+ wait_for_requests
+
+ expect(page).to have_selector('.accept-merge-request')
+ expect(find('.accept-merge-request')['disabled']).not_to be(true)
+ end
+ end
+
context 'view merge request with MWPS enabled but automatically merge fails' do
before do
merge_request.update(
@@ -239,7 +256,7 @@ describe 'Merge request', :js do
end
end
- context 'user can merge into source project but cannot push to fork', js: true do
+ context 'user can merge into source project but cannot push to fork', :js do
let(:fork_project) { create(:project, :public, :repository) }
let(:user2) { create(:user) }
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index f183dd8cb75..1cddd35fd8a 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -12,11 +12,47 @@ describe 'Profile account page' do
visit profile_account_path
end
- it { expect(page).to have_content('Remove account') }
+ it { expect(page).to have_content('Delete account') }
- it 'deletes the account' do
- expect { click_link 'Delete account' }.to change { User.where(id: user.id).count }.by(-1)
- expect(current_path).to eq(new_user_session_path)
+ it 'does not immediately delete the account' do
+ click_button 'Delete account'
+
+ expect(User.exists?(user.id)).to be_truthy
+ end
+
+ it 'deletes user', :js do
+ click_button 'Delete account'
+
+ fill_in 'password', with: '12345678'
+
+ page.within '.popup-dialog' do
+ click_button 'Delete account'
+ end
+
+ expect(page).to have_content('Account scheduled for removal')
+ expect(User.exists?(user.id)).to be_falsy
+ end
+
+ it 'shows invalid password flash message', :js do
+ click_button 'Delete account'
+
+ fill_in 'password', with: 'testing123'
+
+ page.within '.popup-dialog' do
+ click_button 'Delete account'
+ end
+
+ expect(page).to have_content('Invalid password')
+ end
+
+ it 'does not show delete button when user owns a group' do
+ group = create(:group)
+ group.add_owner(user)
+
+ visit profile_account_path
+
+ expect(page).not_to have_button('Delete account')
+ expect(page).to have_content("Your account is currently an owner in these groups: #{group.name}")
end
end
diff --git a/spec/features/profiles/emails_spec.rb b/spec/features/profiles/emails_spec.rb
new file mode 100644
index 00000000000..11cc8aae6f3
--- /dev/null
+++ b/spec/features/profiles/emails_spec.rb
@@ -0,0 +1,71 @@
+require 'rails_helper'
+
+feature 'Profile > Emails' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'User adds an email' do
+ before do
+ visit profile_emails_path
+ end
+
+ scenario 'saves the new email' do
+ fill_in('Email', with: 'my@email.com')
+ click_button('Add email address')
+
+ expect(page).to have_content('my@email.com Unverified')
+ expect(page).to have_content("#{user.email} Verified")
+ expect(page).to have_content('Resend confirmation email')
+ end
+
+ scenario 'does not add a duplicate email' do
+ fill_in('Email', with: user.email)
+ click_button('Add email address')
+
+ email = user.emails.find_by(email: user.email)
+ expect(email).to be_nil
+ expect(page).to have_content('Email has already been taken')
+ end
+ end
+
+ scenario 'User removes email' do
+ user.emails.create(email: 'my@email.com')
+ visit profile_emails_path
+ expect(page).to have_content("my@email.com")
+
+ click_link('Remove')
+ expect(page).not_to have_content("my@email.com")
+ end
+
+ scenario 'User confirms email' do
+ email = user.emails.create(email: 'my@email.com')
+ visit profile_emails_path
+ expect(page).to have_content("#{email.email} Unverified")
+
+ email.confirm
+ expect(email.confirmed?).to be_truthy
+
+ visit profile_emails_path
+ expect(page).to have_content("#{email.email} Verified")
+ end
+
+ scenario 'User re-sends confirmation email' do
+ email = user.emails.create(email: 'my@email.com')
+ visit profile_emails_path
+
+ expect { click_link("Resend confirmation email") }.to change { ActionMailer::Base.deliveries.size }
+ expect(page).to have_content("Confirmation email sent to #{email.email}")
+ end
+
+ scenario 'old unconfirmed emails show Send Confirmation button' do
+ email = user.emails.create(email: 'my@email.com')
+ email.update_attribute(:confirmation_sent_at, nil)
+ visit profile_emails_path
+
+ expect(page).not_to have_content('Resend confirmation email')
+ expect(page).to have_content('Send confirmation email')
+ end
+end
diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb
index 623e4f341c5..59233e92f93 100644
--- a/spec/features/profiles/gpg_keys_spec.rb
+++ b/spec/features/profiles/gpg_keys_spec.rb
@@ -4,7 +4,7 @@ feature 'Profile > GPG Keys' do
let(:user) { create(:user, email: GpgHelpers::User2.emails.first) }
before do
- login_as(user)
+ sign_in(user)
end
describe 'User adds a key' do
@@ -20,6 +20,18 @@ feature 'Profile > GPG Keys' do
expect(page).to have_content('bette.cartwright@example.net Unverified')
expect(page).to have_content(GpgHelpers::User2.fingerprint)
end
+
+ scenario 'with multiple subkeys' do
+ fill_in('Key', with: GpgHelpers::User3.public_key)
+ click_button('Add key')
+
+ expect(page).to have_content('john.doe@example.com Unverified')
+ expect(page).to have_content(GpgHelpers::User3.fingerprint)
+
+ GpgHelpers::User3.subkey_fingerprints.each do |fingerprint|
+ expect(page).to have_content(fingerprint)
+ end
+ end
end
scenario 'User sees their key' do
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index aa71c4dbba4..7d5ba3a7328 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -12,7 +12,7 @@ feature 'Profile > SSH Keys' do
visit profile_keys_path
end
- scenario 'auto-populates the title', js: true do
+ scenario 'auto-populates the title', :js do
fill_in('Key', with: attributes_for(:key).fetch(:key))
expect(page).to have_field("Title", with: "dummy@gitlab.com")
diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb
index 45f78444362..8cb240077eb 100644
--- a/spec/features/profiles/oauth_applications_spec.rb
+++ b/spec/features/profiles/oauth_applications_spec.rb
@@ -7,7 +7,7 @@ describe 'Profile > Applications' do
sign_in(user)
end
- describe 'User manages applications', js: true do
+ describe 'User manages applications', :js do
it 'deletes an application' do
create(:oauth_application, owner: user)
visit oauth_applications_path
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index f3124bbf29e..a572160dae9 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Profile > Personal Access Tokens', js: true do
+describe 'Profile > Personal Access Tokens', :js do
let(:user) { create(:user) }
def active_personal_access_tokens
diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
index 6a4173d43e1..d5fe5bdffc5 100644
--- a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
+++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Profile > Notifications > User changes notified_of_own_activity setting', js: true do
+feature 'Profile > Notifications > User changes notified_of_own_activity setting', :js do
let(:user) { create(:user) }
before do
diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb
index 48c1787c8b7..923ca8b1c80 100644
--- a/spec/features/profiles/user_visits_notifications_tab_spec.rb
+++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User visits the notifications tab', js: true do
+feature 'User visits the notifications tab', :js do
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/artifacts/browse_spec.rb b/spec/features/projects/artifacts/browse_spec.rb
index 42b47cb3301..cb69aff8d5f 100644
--- a/spec/features/projects/artifacts/browse_spec.rb
+++ b/spec/features/projects/artifacts/browse_spec.rb
@@ -4,16 +4,15 @@ feature 'Browse artifact', :js do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ let(:browse_url) do
+ browse_path('other_artifacts_0.1.2')
+ end
def browse_path(path)
browse_project_job_artifacts_path(project, job, path)
end
context 'when visiting old URL' do
- let(:browse_url) do
- browse_path('other_artifacts_0.1.2')
- end
-
before do
visit browse_url.sub('/-/jobs', '/builds')
end
@@ -22,4 +21,47 @@ feature 'Browse artifact', :js do
expect(page.current_path).to eq(browse_url)
end
end
+
+ context 'when browsing a directory with an text file' do
+ let(:txt_entry) { job.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ context 'when the project is public' do
+ it "shows external link icon and styles" do
+ visit browse_url
+
+ link = first('.tree-item-file-external-link')
+
+ expect(page).to have_link('doc_sample.txt', href: file_project_job_artifacts_path(project, job, path: txt_entry.blob.path))
+ expect(link[:target]).to eq('_blank')
+ expect(link[:rel]).to include('noopener')
+ expect(link[:rel]).to include('noreferrer')
+ expect(page).to have_selector('.js-artifact-tree-external-icon')
+ end
+ end
+
+ context 'when the project is private' do
+ let!(:private_project) { create(:project, :private) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: private_project) }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ let(:user) { create(:user) }
+
+ before do
+ private_project.add_developer(user)
+
+ sign_in(user)
+ end
+
+ it 'shows internal link styles' do
+ visit browse_project_job_artifacts_path(private_project, job, 'other_artifacts_0.1.2')
+
+ expect(page).to have_link('doc_sample.txt')
+ expect(page).not_to have_selector('.js-artifact-tree-external-icon')
+ end
+ end
+ end
end
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 89ae891037e..68c4a647958 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -39,7 +39,7 @@ feature 'list of badges' do
end
end
- scenario 'user changes current ref of build status badge', js: true do
+ scenario 'user changes current ref of build status badge', :js do
page.within('.pipeline-status') do
first('.js-project-refs-dropdown').click
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 1160f674974..c12e56d2c3f 100644
--- a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
+++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', js: true do
+feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 62ac9fd0e95..6c625ed17aa 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Editing file blob', js: true do
+feature 'Editing file blob', :js do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/projects/blobs/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb
index 1e3080fa319..9f1fef80ab5 100644
--- a/spec/features/projects/blobs/shortcuts_blob_spec.rb
+++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb
@@ -6,7 +6,7 @@ feature 'Blob shortcuts' do
let(:path) { project.repository.ls_files(project.repository.root_ref)[0] }
let(:sha) { project.repository.commit.sha }
- describe 'On a file(blob)', js: true do
+ describe 'On a file(blob)', :js do
def get_absolute_url(path = "")
"http://#{page.server.host}:#{page.server.port}#{path}"
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index d1f5623554d..941d34dd660 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -46,7 +46,7 @@ describe 'Branches' do
end
describe 'Find branches' do
- it 'shows filtered branches', js: true do
+ it 'shows filtered branches', :js do
visit project_branches_path(project)
fill_in 'branch-search', with: 'fix'
@@ -58,7 +58,7 @@ describe 'Branches' do
end
describe 'Delete unprotected branch' do
- it 'removes branch after confirmation', js: true do
+ it 'removes branch after confirmation', :js do
visit project_branches_path(project)
fill_in 'branch-search', with: 'fix'
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
new file mode 100644
index 00000000000..810f2c39b43
--- /dev/null
+++ b/spec/features/projects/clusters_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+feature 'Clusters', :js do
+ let!(:project) { create(:project, :repository) }
+ let!(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ gitlab_sign_in(user)
+ end
+
+ context 'when user has signed in Google' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:validate_token).and_return(true)
+ end
+
+ context 'when user does not have a cluster and visits cluster index page' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees a new page' do
+ expect(page).to have_button('Create cluster')
+ end
+
+ context 'when user filled form with valid parameters' do
+ before do
+ double.tap do |dbl|
+ allow(dbl).to receive(:status).and_return('RUNNING')
+ allow(dbl).to receive(:self_link)
+ .and_return('projects/gcp-project-12345/zones/us-central1-a/operations/ope-123')
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create).and_return(dbl)
+ end
+
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+
+ fill_in 'cluster_gcp_project_id', with: 'gcp-project-123'
+ fill_in 'cluster_gcp_cluster_name', with: 'dev-cluster'
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a cluster details page and creation status' do
+ expect(page).to have_content('Cluster is being created on Google Container Engine...')
+
+ Gcp::Cluster.last.make_created!
+
+ expect(page).to have_content('Cluster was successfully created on Google Container Engine')
+ end
+ end
+
+ context 'when user filled form with invalid parameters' do
+ before do
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
+ end
+ end
+ end
+
+ context 'when user has a cluster and visits cluster index page' do
+ let!(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service, project: project) }
+
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees an cluster details page' do
+ expect(page).to have_button('Save')
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.gcp_cluster_name)
+ end
+
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-toggle-cluster').click
+ click_button 'Save'
+ end
+
+ it 'user sees the succeccful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ end
+ end
+
+ context 'when user destory the cluster' do
+ before do
+ page.accept_confirm do
+ click_link 'Remove integration'
+ end
+ end
+
+ it 'user sees creation form with the succeccful message' do
+ expect(page).to have_content('Cluster integration was successfully removed.')
+ expect(page).to have_button('Create cluster')
+ end
+ end
+ end
+ end
+
+ context 'when user has not signed in Google' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees a login page' do
+ expect(page).to have_css('.signin-with-google')
+ end
+ end
+end
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index 740331fe42a..9c57626ea1d 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project commit pipelines', js: true do
+feature 'project commit pipelines', :js do
given(:project) { create(:project, :repository) }
background do
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 7086f56bb1b..c11a95732b2 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -64,7 +64,7 @@ describe 'Cherry-pick Commits' do
end
end
- context "I cherry-pick a commit from a different branch", js: true do
+ context "I cherry-pick a commit from a different branch", :js do
it do
find('.header-action-buttons a.dropdown-toggle').click
find(:css, "a[href='#modal-cherry-pick-commit']").click
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 82d73fe8531..87ffc2a0b90 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe "Compare", js: true do
+describe "Compare", :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
index fe8567ce348..36809240f76 100644
--- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb
+++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
@@ -17,7 +17,7 @@ feature 'Developer views empty project instructions' do
expect_instructions_for('http')
end
- scenario 'switches to SSH', js: true do
+ scenario 'switches to SSH', :js do
visit_project
select_protocol('SSH')
@@ -37,7 +37,7 @@ feature 'Developer views empty project instructions' do
expect_instructions_for('ssh')
end
- scenario 'switches to HTTP', js: true do
+ scenario 'switches to HTTP', :js do
visit_project
select_protocol('HTTP')
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
index 17f914c9c17..7a372757523 100644
--- a/spec/features/projects/edit_spec.rb
+++ b/spec/features/projects/edit_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Project edit', js: true do
+feature 'Project edit', :js do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index af7ad365546..610f566c0cf 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -145,7 +145,7 @@ feature 'Environments page', :js do
expect(page).to have_content(action.name.humanize)
end
- it 'allows to play a manual action', js: true do
+ it 'allows to play a manual action', :js do
expect(action).to be_manual
find('.js-dropdown-play-icon-container').click
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 57722276d79..e5282b42a4f 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -6,7 +6,7 @@ describe 'Edit Project Settings' do
let!(:issue) { create(:issue, project: project) }
let(:non_member) { create(:user) }
- describe 'project features visibility selectors', js: true do
+ describe 'project features visibility selectors', :js do
before do
project.team << [member, :master]
sign_in(member)
@@ -163,7 +163,7 @@ describe 'Edit Project Settings' do
end
end
- describe 'repository visibility', js: true do
+ describe 'repository visibility', :js do
before do
project.team << [member, :master]
sign_in(member)
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index f62a9edd37e..84197e45dcb 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'user browses project', js: true do
+feature 'user browses project', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index cebb238dda1..3c3a5326538 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -16,7 +16,7 @@ feature 'User wants to add a Dockerfile file' do
expect(page).to have_css('.dockerfile-selector')
end
- scenario 'user can pick a Dockerfile file from the dropdown', js: true do
+ scenario 'user can pick a Dockerfile file from the dropdown', :js do
find('.js-dockerfile-selector').click
wait_for_requests
diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
index c7e3f657639..25f7e18ac5c 100644
--- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User uses soft wrap whilst editing file', js: true do
+feature 'User uses soft wrap whilst editing file', :js do
before do
user = create(:user)
project = create(:project, :repository)
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
index 7f97fdb8cc9..618725ee781 100644
--- a/spec/features/projects/files/find_file_keyboard_spec.rb
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Find file keyboard shortcuts', js: true do
+feature 'Find file keyboard shortcuts', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index e2044c9d5aa..81d68c3d67c 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -13,7 +13,7 @@ feature 'User wants to add a .gitignore file' do
expect(page).to have_css('.gitignore-selector')
end
- scenario 'user can pick a .gitignore file from the dropdown', js: true do
+ scenario 'user can pick a .gitignore file from the dropdown', :js do
find('.js-gitignore-selector').click
wait_for_requests
within '.gitignore-selector' do
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
index ab242b0b0b5..8e58fa7bd56 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -13,7 +13,7 @@ feature 'User wants to add a .gitlab-ci.yml file' do
expect(page).to have_css('.gitlab-ci-yml-selector')
end
- scenario 'user can pick a template from the dropdown', js: true do
+ scenario 'user can pick a template from the dropdown', :js do
find('.js-gitlab-ci-yml-selector').click
wait_for_requests
within '.gitlab-ci-yml-selector' do
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 95af263bcac..6c5b1086ec1 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project owner creates a license file', js: true do
+feature 'project owner creates a license file', :js do
let(:project_master) { create(:user) }
let(:project) { create(:project, :repository) }
background do
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 7bcab01c739..6c616bf0456 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project owner sees a link to create a license file in empty project', js: true do
+feature 'project owner sees a link to create a license file in empty project', :js do
let(:project_master) { create(:user) }
let(:project) { create(:project) }
background do
diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb
index 48003eeaa87..f95a60e5194 100644
--- a/spec/features/projects/files/template_type_dropdown_spec.rb
+++ b/spec/features/projects/files/template_type_dropdown_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Template type dropdown selector', js: true do
+feature 'Template type dropdown selector', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
index 9bcd5beabb8..64fe350f3dc 100644
--- a/spec/features/projects/files/undo_template_spec.rb
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Template Undo Button', js: true do
+feature 'Template Undo Button', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
index cff3b1f5743..1c988726ae6 100644
--- a/spec/features/projects/gfm_autocomplete_load_spec.rb
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'GFM autocomplete loading', js: true do
+describe 'GFM autocomplete loading', :js do
let(:project) { create(:project) }
before do
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 62d244ff259..05776c50f9d 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
# It looks up for any sensitive word inside the JSON, so if a sensitive word is found
# we''l have to either include it adding the model that includes it to the +safe_list+
# or make sure the attribute is blacklisted in the +import_export.yml+ configuration
-feature 'Import/Export - project export integration test', js: true do
+feature 'Import/Export - project export integration test', :js do
include Select2Helper
include ExportFileHelper
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index e5c7781a096..c928459f911 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Import/Export - project import integration test', js: true do
+feature 'Import/Export - project import integration test', :js do
include Select2Helper
let(:user) { create(:user) }
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
index 691b0e1e4ca..b6a7c3cdcdb 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Import/Export - Namespace export file cleanup', js: true do
+feature 'Import/Export - Namespace export file cleanup', :js do
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 1f9b52dd998..9f67216705d 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
-feature 'issuable templates', js: true do
+feature 'issuable templates', :js do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue_form_location) { '#content-body .issuable-details .detail-page-description' }
@@ -116,15 +118,13 @@ feature 'issuable templates', js: true do
context 'user creates a merge request from a forked project using templates' do
let(:template_content) { 'this is a test "feature-proposal" template' }
let(:fork_user) { create(:user) }
- let(:fork_project) { create(:project, :public, :repository) }
- let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project, target_project: project) }
+ let(:forked_project) { fork_project(project, fork_user, repository: true) }
+ let(:merge_request) { create(:merge_request, :with_diffs, source_project: forked_project, target_project: project) }
background do
sign_out(:user)
project.team << [fork_user, :developer]
- fork_project.team << [fork_user, :master]
- create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
sign_in(fork_user)
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index a4ed589f3de..576870ea0f3 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -299,14 +299,14 @@ feature 'Jobs' do
end
shared_examples 'expected variables behavior' do
- it 'shows variable key and value after click', js: true do
- expect(page).to have_css('.reveal-variables')
+ it 'shows variable key and value after click', :js do
+ expect(page).to have_css('.js-reveal-variables')
expect(page).not_to have_css('.js-build-variable')
expect(page).not_to have_css('.js-build-value')
click_button 'Reveal Variables'
- expect(page).not_to have_css('.reveal-variables')
+ expect(page).not_to have_css('.js-reveal-variables')
expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
end
diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb
index 5716d151250..e8c70dec854 100644
--- a/spec/features/projects/labels/subscription_spec.rb
+++ b/spec/features/projects/labels/subscription_spec.rb
@@ -13,7 +13,7 @@ feature 'Labels subscription' do
sign_in user
end
- scenario 'users can subscribe/unsubscribe to labels', js: true do
+ scenario 'users can subscribe/unsubscribe to labels', :js do
visit project_labels_path(project)
expect(page).to have_content('bug')
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 8f85e972027..d063f5c27b5 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -17,7 +17,7 @@ feature 'Prioritize labels' do
sign_in user
end
- scenario 'user can prioritize a group label', js: true do
+ scenario 'user can prioritize a group label', :js do
visit project_labels_path(project)
expect(page).to have_content('Star labels to start sorting by priority')
@@ -34,7 +34,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can unprioritize a group label', js: true do
+ scenario 'user can unprioritize a group label', :js do
create(:label_priority, project: project, label: feature, priority: 1)
visit project_labels_path(project)
@@ -52,7 +52,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can prioritize a project label', js: true do
+ scenario 'user can prioritize a project label', :js do
visit project_labels_path(project)
expect(page).to have_content('Star labels to start sorting by priority')
@@ -69,7 +69,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can unprioritize a project label', js: true do
+ scenario 'user can unprioritize a project label', :js do
create(:label_priority, project: project, label: bug, priority: 1)
visit project_labels_path(project)
@@ -88,7 +88,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can sort prioritized labels and persist across reloads', js: true do
+ scenario 'user can sort prioritized labels and persist across reloads', :js do
create(:label_priority, project: project, label: bug, priority: 1)
create(:label_priority, project: project, label: feature, priority: 2)
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index c8988aa63a7..6d729f2f85f 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > Group requester cannot request access to project', js: true do
+feature 'Projects > Members > Group requester cannot request access to project', :js do
let(:user) { create(:user) }
let(:owner) { create(:user) }
let(:group) { create(:group, :public, :access_requestable) }
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index 9950272af08..b1053982eee 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > Groups with access list', js: true do
+feature 'Projects > Members > Groups with access list', :js do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index cd621b6b3ce..5f7b4ee0e77 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > Master adds member with expiration date', js: true do
+feature 'Projects > Members > Master adds member with expiration date', :js do
include Select2Helper
include ActiveSupport::Testing::TimeHelpers
diff --git a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
index 6c0b5e279d5..c35ba2d7016 100644
--- a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
@@ -62,4 +62,23 @@ describe 'User accepts a merge request', :js do
wait_for_requests
end
end
+
+ context 'when modifying the merge commit message' do
+ before do
+ merge_request.mark_as_mergeable
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'accepts a merge request' do
+ click_button('Modify commit message')
+ fill_in('Commit message', with: 'wow such merge')
+
+ click_button('Merge')
+
+ page.within('.status-box') do
+ expect(page).to have_content('Merged')
+ end
+ end
+ end
end
diff --git a/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb b/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb
new file mode 100644
index 00000000000..b257f447439
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'User closes a merge requests', :js do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'closes a merge request' do
+ click_link('Close merge request', match: :first)
+
+ expect(page).to have_content(merge_request.title)
+ expect(page).to have_content('Closed by')
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb b/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb
new file mode 100644
index 00000000000..0a952cfc2a9
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe 'User comments on a commit', :js do
+ include MergeRequestDiffHelpers
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+ end
+
+ include_examples 'comment on merge request file'
+end
diff --git a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
new file mode 100644
index 00000000000..f34302f25f8
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
@@ -0,0 +1,172 @@
+require 'spec_helper'
+
+describe 'User comments on a diff', :js do
+ include MergeRequestDiffHelpers
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) do
+ create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
+ end
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(diffs_project_merge_request_path(project, merge_request))
+ end
+
+ context 'when viewing comments' do
+ context 'when toggling inline comments' do
+ context 'in a single file' do
+ it 'hides a comment' do
+ click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ page.within('.files > div:nth-child(3)') do
+ expect(page).to have_content('Line is wrong')
+
+ find('.js-toggle-diff-comments').trigger('click')
+
+ expect(page).not_to have_content('Line is wrong')
+ end
+ end
+ end
+
+ context 'in multiple files' do
+ it 'toggles comments' do
+ click_diff_line(find("[id='#{sample_compare.changes[0][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: 'Line is correct')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.files > div:nth-child(2) .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+
+ click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ # Hide the comment.
+ page.within('.files > div:nth-child(3)') do
+ find('.js-toggle-diff-comments').trigger('click')
+
+ expect(page).not_to have_content('Line is wrong')
+ end
+
+ # At this moment a user should see only one comment.
+ # The other one should be hidden.
+ page.within('.files > div:nth-child(2) .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+
+ # Show the comment.
+ page.within('.files > div:nth-child(3)') do
+ find('.js-toggle-diff-comments').trigger('click')
+ end
+
+ # Now both the comments should be shown.
+ page.within('.files > div:nth-child(3) .note-body > .note-text') do
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.files > div:nth-child(2) .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+
+ # Check the same comments in the side-by-side view.
+ click_link('Side-by-side')
+
+ wait_for_requests
+
+ page.within('.files > div:nth-child(3) .parallel .note-body > .note-text') do
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.files > div:nth-child(2) .parallel .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+ end
+ end
+ end
+ end
+
+ context 'when adding comments' do
+ include_examples 'comment on merge request file'
+ end
+
+ context 'when editing comments' do
+ it 'edits a comment' do
+ click_diff_line(find("[id='#{sample_commit.line_code}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in(:note_note, with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ page.within('.diff-file:nth-of-type(5) .note') do
+ find('.js-note-edit').click
+
+ page.within('.current-note-edit-form') do
+ fill_in('note_note', with: 'Typo, please fix')
+ click_button('Save comment')
+ end
+
+ expect(page).not_to have_button('Save comment', disabled: true)
+ end
+
+ page.within('.diff-file:nth-of-type(5) .note') do
+ expect(page).to have_content('Typo, please fix').and have_no_content('Line is wrong')
+ end
+ end
+ end
+
+ context 'when deleting comments' do
+ it 'deletes a comment' do
+ click_diff_line(find("[id='#{sample_commit.line_code}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in(:note_note, with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('1')
+ end
+
+ page.within('.diff-file:nth-of-type(5) .note') do
+ find('.more-actions').click
+ find('.more-actions .dropdown-menu li', match: :first)
+
+ find('.js-note-delete').click
+ end
+
+ page.within('.merge-request-tabs') do
+ find('.notes-tab').trigger('click')
+ end
+
+ wait_for_requests
+
+ expect(page).not_to have_css('.notes .discussion')
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('0')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb b/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb
new file mode 100644
index 00000000000..2eb652147ce
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe 'User comments on a merge request', :js do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'adds a comment' do
+ page.within('.js-main-target-form') do
+ fill_in(:note_note, with: '# Comment with a header')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.note') do
+ expect(page).to have_content('Comment with a header')
+ expect(page).not_to have_css('#comment-with-a-header')
+ end
+ end
+
+ it 'loads new comment' do
+ # Add new comment in background in order to check
+ # if it's going to be loaded automatically for current user.
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request, author: user, note: 'Line is wrong')
+
+ # Trigger a refresh of notes.
+ execute_script("$(document).trigger('visibilitychange');")
+ wait_for_requests
+
+ page.within('.notes .discussion') do
+ expect(page).to have_content("#{user.name} #{user.to_reference} started a discussion")
+ expect(page).to have_content(sample_commit.line_code_path)
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('1')
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
new file mode 100644
index 00000000000..f285c6c8783
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'User creates a merge request', :js do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_new_merge_request_path(project))
+ end
+
+ it 'creates a merge request' do
+ find('.js-source-branch').click
+ click_link('fix')
+
+ find('.js-target-branch').click
+ click_link('feature')
+
+ click_button('Compare branches')
+
+ fill_in('merge_request_title', with: 'Wiki Feature')
+ click_button('Submit merge request')
+
+ page.within('.merge-request') do
+ expect(page).to have_content('Wiki Feature')
+ end
+
+ wait_for_requests
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
new file mode 100644
index 00000000000..f6e3997383f
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'User edits a merge request', :js do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(edit_project_merge_request_path(project, merge_request))
+ end
+
+ it 'changes the target branch' do
+ expect(page).to have_content('Target branch')
+
+ first('.target_branch').click
+ select('merge-test', from: 'merge_request_target_branch', visible: false)
+ click_button('Save changes')
+
+ expect(page).to have_content("Request to merge #{merge_request.source_branch} into merge-test")
+ expect(page).to have_content("changed target branch from #{merge_request.target_branch} to merge-test")
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
new file mode 100644
index 00000000000..30a80f8e652
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'User manages subscription', :js do
+ let(:project) { create(:project, :public, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'toggles subscription' do
+ subscribe_button = find('.issuable-subscribe-button span')
+
+ expect(subscribe_button).to have_content('Subscribe')
+
+ click_on('Subscribe')
+
+ wait_for_requests
+
+ expect(subscribe_button).to have_content('Unsubscribe')
+
+ click_on('Unsubscribe')
+
+ wait_for_requests
+
+ expect(subscribe_button).to have_content('Subscribe')
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb
new file mode 100644
index 00000000000..ba3c9789da1
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe 'User reopens a merge requests', :js do
+ let(:project) { create(:project, :public, :repository) }
+ let!(:merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'reopens a merge request' do
+ click_link('Reopen merge request', match: :first)
+
+ page.within('.status-box') do
+ expect(page).to have_content('Open')
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
new file mode 100644
index 00000000000..d8d9f7e2a8c
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe 'User sorts merge requests' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:merge_request2) do
+ create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
+ end
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'keeps the sort option' do
+ find('button.dropdown-toggle').click
+
+ page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ click_link('Last updated')
+ end
+
+ visit(merge_requests_dashboard_path(assignee_id: user.id))
+
+ expect(find('.issues-filters')).to have_content('Last updated')
+
+ visit(project_merge_requests_path(project))
+
+ expect(find('.issues-filters')).to have_content('Last updated')
+ end
+
+ context 'when merge requests have awards' do
+ before do
+ create_list(:award_emoji, 2, awardable: merge_request)
+ create(:award_emoji, :downvote, awardable: merge_request)
+
+ create(:award_emoji, awardable: merge_request2)
+ create_list(:award_emoji, 2, :downvote, awardable: merge_request2)
+ end
+
+ it 'sorts by popularity' do
+ find('button.dropdown-toggle').click
+
+ page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ click_link('Popularity')
+ end
+
+ page.within('.mr-list') do
+ page.within('li.merge-request:nth-child(1)') do
+ expect(page).to have_content(merge_request.title)
+ expect(page).to have_content('2 1')
+ end
+
+ page.within('li.merge-request:nth-child(2)') do
+ expect(page).to have_content(merge_request2.title)
+ expect(page).to have_content('1 2')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb
new file mode 100644
index 00000000000..6c695bd7aa9
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User views all merge requests' do
+ let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(project_merge_requests_path(project, state: :all))
+ end
+
+ it 'shows all merge requests' do
+ expect(page).to have_content(merge_request.title).and have_content(closed_merge_request.title)
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb
new file mode 100644
index 00000000000..853809fe87a
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User views closed merge requests' do
+ let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(project_merge_requests_path(project, state: :closed))
+ end
+
+ it 'shows closed merge requests' do
+ expect(page).to have_content(closed_merge_request.title).and have_no_content(merge_request.title)
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_diffs_spec.rb b/spec/features/projects/merge_requests/user_views_diffs_spec.rb
new file mode 100644
index 00000000000..295eb02b625
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_diffs_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe 'User views diffs', :js do
+ let(:merge_request) do
+ create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
+ end
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ visit(diffs_project_merge_request_path(project, merge_request))
+
+ wait_for_requests
+ end
+
+ shared_examples 'unfold diffs' do
+ it 'unfolds diffs' do
+ first('.js-unfold').click
+
+ expect(first('.text-file')).to have_content('.bundle')
+ end
+ end
+
+ it 'shows diffs' do
+ expect(page).to have_css('.tab-content #diffs.active')
+ expect(page).to have_css('#parallel-diff-btn', count: 1)
+ expect(page).to have_css('#inline-diff-btn', count: 1)
+ end
+
+ context 'when in the inline view' do
+ include_examples 'unfold diffs'
+ end
+
+ context 'when in the side-by-side view' do
+ before do
+ click_link('Side-by-side')
+
+ wait_for_requests
+ end
+
+ it 'shows diffs in parallel' do
+ expect(page).to have_css('.parallel')
+ end
+
+ include_examples 'unfold diffs'
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb
new file mode 100644
index 00000000000..eb012694f1e
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User views merged merge requests' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:merged_merge_request) { create(:merged_merge_request, source_project: project, target_project: project) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(project_merge_requests_path(project, state: :merged))
+ end
+
+ it 'shows merged merge requests' do
+ expect(page).to have_content(merged_merge_request.title).and have_no_content(merge_request.title)
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
new file mode 100644
index 00000000000..3aac93eaf7c
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+describe 'User views an open merge request' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project, description: '# Description header')
+ end
+
+ context 'when a merge request does not have repository' do
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'renders both the title and the description' do
+ node = find('.wiki h1 a#user-content-description-header')
+ expect(node[:href]).to end_with('#description-header')
+
+ # Work around a weird Capybara behavior where calling `parent` on a node
+ # returns the whole document, not the node's actual parent element
+ expect(find(:xpath, "#{node.path}/..").text).to eq(merge_request.description[2..-1])
+
+ expect(page).to have_content(merge_request.title).and have_content(merge_request.description)
+ end
+ end
+
+ context 'when a merge request has repository', :js do
+ let(:project) { create(:project, :public, :repository) }
+
+ context 'when rendering description preview' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(edit_project_merge_request_path(project, merge_request))
+ end
+
+ it 'renders empty description preview' do
+ find('.gfm-form').fill_in(:merge_request_description, with: '')
+
+ page.within('.gfm-form') do
+ click_link('Preview')
+
+ expect(find('.js-md-preview')).to have_content('Nothing to preview.')
+ end
+ end
+
+ it 'renders description preview' do
+ find('.gfm-form').fill_in(:merge_request_description, with: ':+1: Nice')
+
+ page.within('.gfm-form') do
+ click_link('Preview')
+
+ expect(find('.js-md-preview')).to have_css('gl-emoji')
+ end
+
+ expect(find('.gfm-form')).to have_css('.js-md-preview').and have_link('Write')
+ expect(find('#merge_request_description', visible: false)).not_to be_visible
+ end
+ end
+
+ context 'when the branch is rebased on the target' do
+ let(:merge_request) { create(:merge_request, :rebased, source_project: project, target_project: project) }
+
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'does not show diverged commits count' do
+ page.within('.mr-source-target') do
+ expect(page).not_to have_content(/([0-9]+ commit[s]? behind)/)
+ end
+ end
+ end
+
+ context 'when the branch is diverged on the target' do
+ let(:merge_request) { create(:merge_request, :diverged, source_project: project, target_project: project) }
+
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'shows diverged commits count' do
+ page.within('.mr-source-target') do
+ expect(page).to have_content(/([0-9]+ commits behind)/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
new file mode 100644
index 00000000000..07b8c1ef479
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe 'User views open merge requests' do
+ let(:project) { create(:project, :public, :repository) }
+
+ context "when the target branch is the project's default branch" do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+
+ before do
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows open merge requests' do
+ expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title)
+ end
+
+ it 'does not show target branch name' do
+ expect(page).to have_content(merge_request.title)
+ expect(find('.issuable-info')).not_to have_content(project.default_branch)
+ end
+ end
+
+ context "when the target branch is different from the project's default branch" do
+ let!(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'feature_conflict')
+ end
+
+ before do
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows target branch name' do
+ expect(page).to have_content(merge_request.target_branch)
+ end
+ end
+
+ context 'when a merge request has pipelines' do
+ let!(:build) { create :ci_build, pipeline: pipeline }
+
+ let(:merge_request) do
+ create(:merge_request_with_diffs,
+ source_project: project,
+ target_project: project,
+ source_branch: 'merge-test')
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ head_pipeline_of: merge_request)
+ end
+
+ before do
+ project.enable_ci
+
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows pipeline status' do
+ page.within('.mr-list') do
+ expect(page).to have_link('Pipeline: pending')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index f7b40cb1820..c35b0840248 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -162,6 +162,16 @@ describe 'Pipelines', :js do
expect(page).to have_selector(
%Q{span[data-original-title="#{pipeline.yaml_errors}"]})
end
+
+ it 'contains badge that indicates failure reason' do
+ expect(page).to have_content 'error'
+ end
+
+ it 'contains badge with tooltip which contains failure reason' do
+ expect(pipeline.failure_reason?).to eq true
+ expect(page).to have_selector(
+ %Q{span[data-original-title="#{pipeline.present.failure_reason}"]})
+ end
end
context 'with manual actions' do
@@ -443,7 +453,7 @@ describe 'Pipelines', :js do
visit new_project_pipeline_path(project)
end
- context 'for valid commit', js: true do
+ context 'for valid commit', :js do
before do
click_button project.default_branch
@@ -491,7 +501,7 @@ describe 'Pipelines', :js do
end
describe 'find pipelines' do
- it 'shows filtered pipelines', js: true do
+ it 'shows filtered pipelines', :js do
click_button project.default_branch
page.within '.dropdown-menu' do
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
index 06568817757..15a5cd9990b 100644
--- a/spec/features/projects/project_settings_spec.rb
+++ b/spec/features/projects/project_settings_spec.rb
@@ -10,7 +10,7 @@ describe 'Edit Project Settings' do
sign_in(user)
end
- describe 'Project settings section', js: true do
+ describe 'Project settings section', :js do
it 'shows errors for invalid project name' do
visit edit_project_path(project)
fill_in 'project_name_edit', with: 'foo&bar'
@@ -125,7 +125,7 @@ describe 'Edit Project Settings' do
end
end
- describe 'Transfer project section', js: true do
+ describe 'Transfer project section', :js do
let!(:project) { create(:project, :repository, namespace: user.namespace, name: 'gitlabhq') }
let!(:group) { create(:group) }
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index f0a23729220..f8695403857 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Ref switcher', js: true do
+feature 'Ref switcher', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb
index d932c4e4d9a..cbdb7973ac8 100644
--- a/spec/features/projects/settings/integration_settings_spec.rb
+++ b/spec/features/projects/settings/integration_settings_spec.rb
@@ -76,7 +76,7 @@ feature 'Integration settings' do
expect(page).to have_content(url)
end
- scenario 'test existing webhook', js: true do
+ scenario 'test existing webhook', :js do
WebMock.stub_request(:post, hook.url)
visit integrations_path
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index 975d204e75e..de8fbb15b9c 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -22,7 +22,7 @@ feature "Pipelines settings" do
context 'for master' do
given(:role) { :master }
- scenario 'be allowed to change', js: true do
+ scenario 'be allowed to change', :js do
fill_in('Test coverage parsing', with: 'coverage_regex')
click_on 'Save changes'
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 15180d4b498..a4fefb0d0e7 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -23,7 +23,7 @@ feature 'Repository settings' do
context 'for master' do
given(:role) { :master }
- context 'Deploy Keys', js: true do
+ context 'Deploy Keys', :js do
let(:private_deploy_key) { create(:deploy_key, title: 'private_deploy_key', public: false) }
let(:public_deploy_key) { create(:another_deploy_key, title: 'public_deploy_key', public: true) }
let(:new_ssh_key) { attributes_for(:key)[:key] }
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
index 37ee6255bd1..1c3b84d0114 100644
--- a/spec/features/projects/settings/visibility_settings_spec.rb
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Visibility settings', js: true do
+feature 'Visibility settings', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace, visibility_level: 20) }
diff --git a/spec/features/projects/show_project_spec.rb b/spec/features/projects/show_project_spec.rb
index 1bc6fae9e7f..0b94c9eae5d 100644
--- a/spec/features/projects/show_project_spec.rb
+++ b/spec/features/projects/show_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Project show page', feature: true do
+describe 'Project show page', :feature do
context 'when project pending delete' do
let(:project) { create(:project, :empty_repo, pending_delete: true) }
diff --git a/spec/features/projects/user_browses_files_spec.rb b/spec/features/projects/user_browses_files_spec.rb
index b7a0b72db50..f43b11c9485 100644
--- a/spec/features/projects/user_browses_files_spec.rb
+++ b/spec/features/projects/user_browses_files_spec.rb
@@ -76,7 +76,7 @@ describe 'User browses files' do
expect(page).to have_content('LICENSE')
end
- it 'shows files from a repository with apostroph in its name', js: true do
+ it 'shows files from a repository with apostroph in its name', :js do
first('.js-project-refs-dropdown').click
page.within('.project-refs-form') do
@@ -91,7 +91,7 @@ describe 'User browses files' do
expect(page).not_to have_content('Loading commit data...')
end
- it 'shows the code with a leading dot in the directory', js: true do
+ it 'shows the code with a leading dot in the directory', :js do
first('.js-project-refs-dropdown').click
page.within('.project-refs-form') do
@@ -117,7 +117,7 @@ describe 'User browses files' do
click_link('.gitignore')
end
- it 'shows a file content', js: true do
+ it 'shows a file content', :js do
wait_for_requests
expect(page).to have_content('*.rbc')
end
@@ -168,7 +168,7 @@ describe 'User browses files' do
visit(tree_path_root_ref)
end
- it 'shows a preview of a file content', js: true do
+ it 'shows a preview of a file content', :js do
find('.add-to-tree').click
click_link('Upload file')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg'))
diff --git a/spec/features/projects/user_creates_directory_spec.rb b/spec/features/projects/user_creates_directory_spec.rb
index 1ba5d83eadf..052cb3188c5 100644
--- a/spec/features/projects/user_creates_directory_spec.rb
+++ b/spec/features/projects/user_creates_directory_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User creates a directory', js: true do
+feature 'User creates a directory', :js do
let(:fork_message) do
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
@@ -79,7 +79,7 @@ feature 'User creates a directory', js: true do
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Create directory')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
end
diff --git a/spec/features/projects/user_creates_files_spec.rb b/spec/features/projects/user_creates_files_spec.rb
index 3d335687510..cbe70a93942 100644
--- a/spec/features/projects/user_creates_files_spec.rb
+++ b/spec/features/projects/user_creates_files_spec.rb
@@ -59,7 +59,7 @@ describe 'User creates files' do
expect(page).to have_selector('.file-editor')
end
- it 'creates and commit a new file', js: true do
+ it 'creates and commit a new file', :js do
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
@@ -74,7 +74,7 @@ describe 'User creates files' do
expect(page).to have_content('*.rbca')
end
- it 'creates and commit a new file with new lines at the end of file', js: true do
+ it 'creates and commit a new file with new lines at the end of file', :js do
execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
@@ -89,7 +89,7 @@ describe 'User creates files' do
expect(evaluate_script('ace.edit("editor").getValue()')).to eq("Sample\n\n\n")
end
- it 'creates and commit a new file with a directory name', js: true do
+ it 'creates and commit a new file with a directory name', :js do
fill_in(:file_name, with: 'foo/bar/baz.txt')
expect(page).to have_selector('.file-editor')
@@ -105,7 +105,7 @@ describe 'User creates files' do
expect(page).to have_content('*.rbca')
end
- it 'creates and commit a new file specifying a new branch', js: true do
+ it 'creates and commit a new file specifying a new branch', :js do
expect(page).to have_selector('.file-editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
@@ -130,7 +130,7 @@ describe 'User creates files' do
visit(project2_tree_path_root_ref)
end
- it 'creates and commit new file in forked project', js: true do
+ it 'creates and commit new file in forked project', :js do
find('.add-to-tree').click
click_link('New file')
@@ -142,7 +142,7 @@ describe 'User creates files' do
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
expect(page).to have_content('New commit message')
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index 1c3791f63ac..4a152572502 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User creates a project', js: true do
+feature 'User creates a project', :js do
let(:user) { create(:user) }
before do
diff --git a/spec/features/projects/user_deletes_files_spec.rb b/spec/features/projects/user_deletes_files_spec.rb
index 95cd316be0e..9e4e92ec076 100644
--- a/spec/features/projects/user_deletes_files_spec.rb
+++ b/spec/features/projects/user_deletes_files_spec.rb
@@ -21,7 +21,7 @@ describe 'User deletes files' do
visit(project_tree_path_root_ref)
end
- it 'deletes the file', js: true do
+ it 'deletes the file', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
@@ -41,7 +41,7 @@ describe 'User deletes files' do
visit(project2_tree_path_root_ref)
end
- it 'deletes the file in a forked project', js: true do
+ it 'deletes the file in a forked project', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
@@ -59,7 +59,7 @@ describe 'User deletes files' do
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Delete file')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
expect(page).to have_content('New commit message')
diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb
index 19954313c23..e8d83a661d4 100644
--- a/spec/features/projects/user_edits_files_spec.rb
+++ b/spec/features/projects/user_edits_files_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe 'User edits files' do
+ include ProjectForksHelper
let(:project) { create(:project, :repository, name: 'Shop') }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) }
@@ -17,7 +18,7 @@ describe 'User edits files' do
visit(project_tree_path_root_ref)
end
- it 'inserts a content of a file', js: true do
+ it 'inserts a content of a file', :js do
click_link('.gitignore')
find('.js-edit-blob').click
find('.file-editor', match: :first)
@@ -34,7 +35,7 @@ describe 'User edits files' do
expect(page).not_to have_link('edit')
end
- it 'commits an edited file', js: true do
+ it 'commits an edited file', :js do
click_link('.gitignore')
find('.js-edit-blob').click
find('.file-editor', match: :first)
@@ -50,7 +51,7 @@ describe 'User edits files' do
expect(page).to have_content('*.rbca')
end
- it 'commits an edited file to a new branch', js: true do
+ it 'commits an edited file to a new branch', :js do
click_link('.gitignore')
find('.js-edit-blob').click
@@ -68,7 +69,7 @@ describe 'User edits files' do
expect(page).to have_content('*.rbca')
end
- it 'shows the diff of an edited file', js: true do
+ it 'shows the diff of an edited file', :js do
click_link('.gitignore')
find('.js-edit-blob').click
find('.file-editor', match: :first)
@@ -86,7 +87,7 @@ describe 'User edits files' do
visit(project2_tree_path_root_ref)
end
- it 'inserts a content of a file in a forked project', js: true do
+ it 'inserts a content of a file in a forked project', :js do
click_link('.gitignore')
find('.js-edit-blob').click
@@ -107,7 +108,7 @@ describe 'User edits files' do
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
end
- it 'commits an edited file in a forked project', js: true do
+ it 'commits an edited file in a forked project', :js do
click_link('.gitignore')
find('.js-edit-blob').click
@@ -122,7 +123,7 @@ describe 'User edits files' do
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
@@ -130,5 +131,34 @@ describe 'User edits files' do
expect(page).to have_content('New commit message')
end
+
+ context 'when the user already had a fork of the project', :js do
+ let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) }
+ before do
+ visit(project2_tree_path_root_ref)
+ end
+
+ it 'links to the forked project for editing' do
+ click_link('.gitignore')
+ find('.js-edit-blob').click
+
+ expect(page).not_to have_link('Fork')
+ expect(page).not_to have_button('Cancel')
+
+ execute_script("ace.edit('editor').setValue('*.rbca')")
+ fill_in(:commit_message, with: 'Another commit', visible: true)
+ click_button('Commit changes')
+
+ fork = user.fork_of(project2)
+
+ expect(current_path).to eq(project_new_merge_request_path(fork))
+
+ wait_for_requests
+
+ expect(page).to have_content('Another commit')
+ expect(page).to have_content("From #{forked_project.full_path}")
+ expect(page).to have_content("into #{project2.full_path}")
+ end
+ end
end
end
diff --git a/spec/features/projects/user_interacts_with_stars_spec.rb b/spec/features/projects/user_interacts_with_stars_spec.rb
index 0ac3f8181fa..d9d2e0ab171 100644
--- a/spec/features/projects/user_interacts_with_stars_spec.rb
+++ b/spec/features/projects/user_interacts_with_stars_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'User interacts with project stars' do
let(:project) { create(:project, :public, :repository) }
- context 'when user is signed in', js: true do
+ context 'when user is signed in', :js do
let(:user) { create(:user) }
before do
diff --git a/spec/features/projects/user_replaces_files_spec.rb b/spec/features/projects/user_replaces_files_spec.rb
index e284fdefd4f..245b6aa285b 100644
--- a/spec/features/projects/user_replaces_files_spec.rb
+++ b/spec/features/projects/user_replaces_files_spec.rb
@@ -23,7 +23,7 @@ describe 'User replaces files' do
visit(project_tree_path_root_ref)
end
- it 'replaces an existed file with a new one', js: true do
+ it 'replaces an existed file with a new one', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
@@ -49,7 +49,7 @@ describe 'User replaces files' do
visit(project2_tree_path_root_ref)
end
- it 'replaces an existed file with a new one in a forked project', js: true do
+ it 'replaces an existed file with a new one in a forked project', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
@@ -74,7 +74,7 @@ describe 'User replaces files' do
expect(page).to have_content('Replacement file commit message')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
diff --git a/spec/features/projects/user_uploads_files_spec.rb b/spec/features/projects/user_uploads_files_spec.rb
index 98871317ca3..ae51901adc6 100644
--- a/spec/features/projects/user_uploads_files_spec.rb
+++ b/spec/features/projects/user_uploads_files_spec.rb
@@ -23,7 +23,7 @@ describe 'User uploads files' do
visit(project_tree_path_root_ref)
end
- it 'uploads and commit a new file', js: true do
+ it 'uploads and commit a new file', :js do
find('.add-to-tree').click
click_link('Upload file')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
@@ -39,6 +39,9 @@ describe 'User uploads files' do
expect(current_path).to eq(project_new_merge_request_path(project))
click_link('Changes')
+ find("a[data-action='diffs']", text: 'Changes').click
+
+ wait_for_requests
expect(page).to have_content('Lorem ipsum dolor sit amet')
expect(page).to have_content('Sed ut perspiciatis unde omnis')
@@ -51,7 +54,7 @@ describe 'User uploads files' do
visit(project2_tree_path_root_ref)
end
- it 'uploads and commit a new fileto a forked project', js: true do
+ it 'uploads and commit a new file to a forked project', :js do
find('.add-to-tree').click
click_link('Upload file')
@@ -69,11 +72,13 @@ describe 'User uploads files' do
expect(page).to have_content('New commit message')
- fork = user.fork_of(project2)
+ fork = user.fork_of(project2.reload)
expect(current_path).to eq(project_new_merge_request_path(fork))
- click_link('Changes')
+ find("a[data-action='diffs']", text: 'Changes').click
+
+ wait_for_requests
expect(page).to have_content('Lorem ipsum dolor sit amet')
expect(page).to have_content('Sed ut perspiciatis unde omnis')
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index 2a316a0d0db..7f547a4ca1f 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'View on environment', js: true do
+describe 'View on environment', :js do
let(:branch_name) { 'feature' }
let(:file_path) { 'files/ruby/feature.rb' }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 9a4ccf3c54d..78c350c8ee4 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Wiki > User previews markdown changes', js: true do
+feature 'Projects > Wiki > User previews markdown changes', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:wiki_content) do
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 81f7ab80a04..ac62280e4ca 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Project' do
+ include ProjectForksHelper
+
describe 'creating from template' do
let(:user) { create(:user) }
let(:template) { Gitlab::ProjectTemplate.find(:rails) }
@@ -55,13 +57,12 @@ feature 'Project' do
end
end
- describe 'remove forked relationship', js: true do
+ describe 'remove forked relationship', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { fork_project(create(:project, :public), user, namespace_id: user.namespace) }
before do
sign_in user
- create(:forked_project_link, forked_to_project: project)
visit edit_project_path(project)
end
@@ -71,12 +72,61 @@ feature 'Project' do
remove_with_confirm('Remove fork relationship', project.path)
expect(page).to have_content 'The fork relationship has been removed.'
- expect(project.forked?).to be_falsey
+ expect(project.reload.forked?).to be_falsey
expect(page).not_to have_content 'Remove fork relationship'
end
end
- describe 'removal', js: true do
+ describe 'showing information about source of a project fork' do
+ let(:user) { create(:user) }
+ let(:base_project) { create(:project, :public, :repository) }
+ let(:forked_project) { fork_project(base_project, user, repository: true) }
+
+ before do
+ sign_in user
+ end
+
+ it 'shows a link to the source project when it is available' do
+ visit project_path(forked_project)
+
+ expect(page).to have_content('Forked from')
+ expect(page).to have_link(base_project.full_name)
+ end
+
+ it 'does not contain fork network information for the root project' do
+ forked_project
+
+ visit project_path(base_project)
+
+ expect(page).not_to have_content('In fork network of')
+ expect(page).not_to have_content('Forked from')
+ end
+
+ it 'shows the name of the deleted project when the source was deleted' do
+ forked_project
+ Projects::DestroyService.new(base_project, base_project.owner).execute
+
+ visit project_path(forked_project)
+
+ expect(page).to have_content("Forked from #{base_project.full_name} (deleted)")
+ end
+
+ context 'a fork of a fork' do
+ let(:fork_of_fork) { fork_project(forked_project, user, repository: true) }
+
+ it 'links to the base project if the source project is removed' do
+ fork_of_fork
+ Projects::DestroyService.new(forked_project, user).execute
+
+ visit project_path(fork_of_fork)
+
+ expect(page).to have_content("Forked from")
+ expect(page).to have_link(base_project.full_name)
+ end
+ end
+ end
+
+ describe 'removal', :js do
let(:user) { create(:user, username: 'test', name: 'test') }
let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index bf9885f73bd..2ab1eda90f1 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -60,6 +60,31 @@ feature 'Protected Branches', :js do
expect(page).to have_content('No branches to show')
end
end
+
+ describe "Saved defaults" do
+ it "keeps the allowed to merge and push dropdowns defaults based on the previous selection" do
+ visit project_protected_branches_path(project)
+ form = '.js-new-protected-branch'
+
+ within form do
+ find(".js-allowed-to-merge").trigger('click')
+ click_link 'No one'
+ find(".js-allowed-to-push").trigger('click')
+ click_link 'Developers + Masters'
+ end
+
+ visit project_protected_branches_path(project)
+
+ within form do
+ page.within(".js-allowed-to-merge") do
+ expect(page.find(".dropdown-toggle-text")).to have_content("No one")
+ end
+ page.within(".js-allowed-to-push") do
+ expect(page.find(".dropdown-toggle-text")).to have_content("Developers + Masters")
+ end
+ end
+ end
+ end
end
context 'logged in as admin' do
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index 8abd4403065..8cc6f17b8d9 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Protected Tags', js: true do
+feature 'Protected Tags', :js do
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb
index 3a229612235..3a2768c424f 100644
--- a/spec/features/snippets/internal_snippet_spec.rb
+++ b/spec/features/snippets/internal_snippet_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Internal Snippets', js: true do
+feature 'Internal Snippets', :js do
let(:internal_snippet) { create(:personal_snippet, :internal) }
describe 'normal user' do
diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb
index 39d79a3327b..1455345bd56 100644
--- a/spec/features/tags/master_creates_tag_spec.rb
+++ b/spec/features/tags/master_creates_tag_spec.rb
@@ -55,7 +55,7 @@ feature 'Master creates tag' do
end
end
- scenario 'opens dropdown for ref', js: true do
+ scenario 'opens dropdown for ref', :js do
click_link 'New tag'
ref_row = find('.form-group:nth-of-type(2) .col-sm-10')
page.within ref_row do
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index 80750c904b5..f5b3774122b 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -10,7 +10,7 @@ feature 'Master deletes tag' do
visit project_tags_path(project)
end
- context 'from the tags list page', js: true do
+ context 'from the tags list page', :js do
scenario 'deletes the tag' do
expect(page).to have_content 'v1.1.0'
@@ -34,7 +34,7 @@ feature 'Master deletes tag' do
end
end
- context 'when pre-receive hook fails', js: true do
+ context 'when pre-receive hook fails', :js do
context 'when Gitaly operation_user_delete_tag feature is enabled' do
before do
allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag)
@@ -48,7 +48,7 @@ feature 'Master deletes tag' do
end
end
- context 'when Gitaly operation_user_delete_tag feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do
before do
allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
.and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags')
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index aeb0534b733..2dc3c5e3927 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Task Lists' do
include Warden::Test::Helpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -63,7 +63,7 @@ feature 'Task Lists' do
end
describe 'for Issues' do
- describe 'multiple tasks', js: true do
+ describe 'multiple tasks', :js do
let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
it 'renders' do
@@ -103,7 +103,7 @@ feature 'Task Lists' do
end
end
- describe 'single incomplete task', js: true do
+ describe 'single incomplete task', :js do
let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) }
it 'renders' do
@@ -122,7 +122,7 @@ feature 'Task Lists' do
end
end
- describe 'single complete task', js: true do
+ describe 'single complete task', :js do
let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) }
it 'renders' do
@@ -141,7 +141,7 @@ feature 'Task Lists' do
end
end
- describe 'nested tasks', js: true do
+ describe 'nested tasks', :js do
let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) }
before do
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 47664de469a..548d8372a07 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Triggers', js: true do
+feature 'Triggers', :js do
let(:trigger_title) { 'trigger desc' }
let(:user) { create(:user) }
let(:user2) { create(:user) }
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
index e1c95590af1..1261ffdc2ee 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -14,20 +14,20 @@ feature 'User uploads file to note' do
end
context 'before uploading' do
- it 'shows "Attach a file" button', js: true do
+ it 'shows "Attach a file" button', :js do
expect(page).to have_button('Attach a file')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
end
context 'uploading is in progress' do
- it 'shows "Cancel" button on uploading', js: true do
+ it 'shows "Cancel" button on uploading', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
expect(page).to have_button('Cancel')
end
- it 'cancels uploading on clicking to "Cancel" button', js: true do
+ it 'cancels uploading on clicking to "Cancel" button', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
click_button 'Cancel'
@@ -37,20 +37,20 @@ feature 'User uploads file to note' do
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
- it 'shows "Attaching a file" message on uploading 1 file', js: true do
+ it 'shows "Attaching a file" message on uploading 1 file', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
end
- it 'shows "Attaching 2 files" message on uploading 2 file', js: true do
+ it 'shows "Attaching 2 files" message on uploading 2 file', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'),
Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -')
end
- it 'shows error message, "retry" and "attach a new file" link a if file is too big', js: true do
+ it 'shows error message, "retry" and "attach a new file" link a if file is too big', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4')], 0.01)
error_text = 'File is too big (0.06MiB). Max filesize: 0.01MiB.'
@@ -63,7 +63,7 @@ feature 'User uploads file to note' do
end
context 'uploading is complete' do
- it 'shows "Attach a file" button on uploading complete', js: true do
+ it 'shows "Attach a file" button on uploading complete', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
wait_for_requests
@@ -71,7 +71,7 @@ feature 'User uploads file to note' do
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
- scenario 'they see the attached file', js: true do
+ scenario 'they see the attached file', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
click_button 'Comment'
wait_for_requests
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index 13760b4c2fc..8c697e33436 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Snippets tab on a user profile', js: true do
+describe 'Snippets tab on a user profile', :js do
context 'when the user has snippets' do
let(:user) { create(:user) }
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 15b89dac572..0252c957c95 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Users', js: true do
+feature 'Users', :js do
let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
scenario 'GET /users/sign_in creates a new user account' do
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index 6794bf4f4ba..5d8e818f7bf 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Project variables', js: true do
+describe 'Project variables', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb
new file mode 100644
index 00000000000..c81bfd7932c
--- /dev/null
+++ b/spec/finders/merge_request_target_project_finder_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe MergeRequestTargetProjectFinder do
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ subject(:finder) { described_class.new(current_user: user, source_project: forked_project) }
+
+ shared_examples 'finding related projects' do
+ it 'finds sibling projects and base project' do
+ other_fork
+
+ expect(finder.execute).to contain_exactly(base_project, other_fork, forked_project)
+ end
+
+ it 'does not include projects that have merge requests turned off' do
+ other_fork.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
+ base_project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
+
+ expect(finder.execute).to contain_exactly(forked_project)
+ end
+ end
+
+ context 'public projects' do
+ let(:base_project) { create(:project, :public, path: 'base') }
+ let(:forked_project) { fork_project(base_project) }
+ let(:other_fork) { fork_project(base_project) }
+
+ it_behaves_like 'finding related projects'
+ end
+
+ context 'private projects' do
+ let(:base_project) { create(:project, :private, path: 'base') }
+ let(:forked_project) { fork_project(base_project, base_project.owner) }
+ let(:other_fork) { fork_project(base_project, base_project.owner) }
+
+ context 'when the user is a member of all projects' do
+ before do
+ base_project.add_developer(user)
+ forked_project.add_developer(user)
+ other_fork.add_developer(user)
+ end
+
+ it_behaves_like 'finding related projects'
+ end
+
+ it 'only finds the projects the user is a member of' do
+ other_fork.add_developer(user)
+ base_project.add_developer(user)
+
+ expect(finder.execute).to contain_exactly(other_fork, base_project)
+ end
+ end
+end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 95f445e7905..883bdf3746a 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -1,12 +1,18 @@
require 'spec_helper'
describe MergeRequestsFinder do
+ include ProjectForksHelper
+
let(:user) { create :user }
let(:user2) { create :user }
- let(:project1) { create(:project) }
- let(:project2) { create(:project, forked_from_project: project1) }
- let(:project3) { create(:project, :archived, forked_from_project: project1) }
+ let(:project1) { create(:project, :public) }
+ let(:project2) { fork_project(project1, user) }
+ let(:project3) do
+ p = fork_project(project1, user)
+ p.update!(archived: true)
+ p
+ end
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') }
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
new file mode 100644
index 00000000000..1f255a17881
--- /dev/null
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -0,0 +1,11 @@
+{
+ "type": "object",
+ "required" : [
+ "status"
+ ],
+ "properties" : {
+ "status": { "type": "string" },
+ "status_reason": { "type": ["string", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json
index 30b4e56bc98..ba094ba1657 100644
--- a/spec/fixtures/api/schemas/entities/merge_request.json
+++ b/spec/fixtures/api/schemas/entities/merge_request.json
@@ -46,6 +46,7 @@
"branch_missing": { "type": "boolean" },
"has_conflicts": { "type": "boolean" },
"can_be_merged": { "type": "boolean" },
+ "mergeable": { "type": "boolean" },
"project_archived": { "type": "boolean" },
"only_allow_merge_if_pipeline_succeeds": { "type": "boolean" },
"has_ci": { "type": "boolean" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json
index 03c422ab023..5c08dbc3b96 100644
--- a/spec/fixtures/api/schemas/public_api/v4/issues.json
+++ b/spec/fixtures/api/schemas/public_api/v4/issues.json
@@ -9,6 +9,7 @@
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
+ "discussion_locked": { "type": ["boolean", "null"] },
"closed_at": { "type": "date" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
index 31b3f4ba946..5828be5255b 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -72,6 +72,7 @@
"user_notes_count": { "type": "integer" },
"should_remove_source_branch": { "type": ["boolean", "null"] },
"force_remove_source_branch": { "type": ["boolean", "null"] },
+ "discussion_locked": { "type": ["boolean", "null"] },
"web_url": { "type": "uri" },
"time_stats": {
"time_estimate": { "type": "integer" },
diff --git a/spec/fixtures/trace/trace_with_sections b/spec/fixtures/trace/trace_with_sections
new file mode 100644
index 00000000000..21dff3928c3
--- /dev/null
+++ b/spec/fixtures/trace/trace_with_sections
@@ -0,0 +1,15 @@
+Running with gitlab-runner dev (HEAD)
+ on kitsune minikube (a21b584f)
+WARNING: Namespace is empty, therefore assuming 'default'.
+Using Kubernetes namespace: default
+Using Kubernetes executor with image alpine:3.4 ...
+section_start:1506004954:prepare_script Waiting for pod default/runner-a21b584f-project-1208199-concurrent-0sg03f to be running, status is Pending
+Running on runner-a21b584f-project-1208199-concurrent-0sg03f via kitsune.local...
+section_end:1506004957:prepare_script section_start:1506004957:get_sources Cloning repository...
+Cloning into '/nolith/ci-tests'...
+Checking out dddd7a6e as master...
+Skipping Git submodules setup
+section_end:1506004958:get_sources section_start:1506004958:restore_cache section_end:1506004958:restore_cache section_start:1506004958:download_artifacts section_end:1506004958:download_artifacts section_start:1506004958:build_script $ whoami
+root
+section_end:1506004959:build_script section_start:1506004959:after_script section_end:1506004959:after_script section_start:1506004959:archive_cache section_end:1506004959:archive_cache section_start:1506004959:upload_artifacts section_end:1506004959:upload_artifacts Job succeeded
+
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 10bc5f2ecd2..87ae1fa5660 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -57,15 +57,17 @@ describe ApplicationHelper do
end
describe 'project_icon' do
+ let(:asset_host) { 'http://assets' }
+
it 'returns an url for the avatar' do
- project = create(:project, avatar: File.open(uploaded_image_temp_path))
+ project = create(:project, :public, avatar: File.open(uploaded_image_temp_path))
avatar_url = "/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon(project.full_path).to_s)
.to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
- allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
- avatar_url = "#{gitlab_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
+ allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
+ avatar_url = "#{asset_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon(project.full_path).to_s)
.to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 76e5964ccf7..97f0ed4904e 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
describe GroupsHelper do
include ApplicationHelper
+ let(:asset_host) { 'http://assets' }
+
describe 'group_icon' do
avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
@@ -10,14 +12,53 @@ describe GroupsHelper do
group = create(:group)
group.avatar = fixture_file_upload(avatar_file_path)
group.save!
- expect(group_icon(group.path).to_s)
+
+ avatar_url = "/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif"
+
+ expect(helper.group_icon(group).to_s)
+ .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
+
+ allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
+ avatar_url = "#{asset_host}/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif"
+
+ expect(helper.group_icon(group).to_s)
+ .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
+ end
+ end
+
+ describe 'group_icon_url' do
+ avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
+
+ it 'returns an url for the avatar' do
+ group = create(:group)
+ group.avatar = fixture_file_upload(avatar_file_path)
+ group.save!
+ expect(group_icon_url(group.path).to_s)
+ .to match("/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif")
+ end
+
+ it 'returns an CDN url for the avatar' do
+ allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
+ group = create(:group)
+ group.avatar = fixture_file_upload(avatar_file_path)
+ group.save!
+ expect(group_icon_url(group.path).to_s)
+ .to match("#{asset_host}/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif")
+ end
+
+ it 'returns an based url for the avatar if private' do
+ allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
+ group = create(:group, :private)
+ group.avatar = fixture_file_upload(avatar_file_path)
+ group.save!
+ expect(group_icon_url(group.path).to_s)
.to match("/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif")
end
it 'gives default avatar_icon when no avatar is present' do
group = create(:group)
group.save!
- expect(group_icon(group.path)).to match_asset_path('group_avatar.png')
+ expect(group_icon_url(group.path)).to match_asset_path('group_avatar.png')
end
end
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 7d1c17909bf..fd7900c32f4 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe MergeRequestsHelper do
+ include ProjectForksHelper
describe 'ci_build_details_path' do
let(:project) { create(:project) }
let(:merge_request) { MergeRequest.new }
@@ -31,10 +32,10 @@ describe MergeRequestsHelper do
describe 'within different projects' do
let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
- let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) }
+ let(:forked_project) { fork_project(project) }
+ let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project) }
subject { format_mr_branch_names(merge_request) }
- let(:source_title) { "#{fork_project.full_path}:#{merge_request.source_branch}" }
+ let(:source_title) { "#{forked_project.full_path}:#{merge_request.source_branch}" }
let(:target_title) { "#{project.full_path}:#{merge_request.target_branch}" }
it { is_expected.to eq([source_title, target_title]) }
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 641971485de..5777b5c4025 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -63,7 +63,7 @@ describe ProjectsHelper do
end
end
- describe "#project_list_cache_key", clean_gitlab_redis_shared_state: true do
+ describe "#project_list_cache_key", :clean_gitlab_redis_shared_state do
let(:project) { create(:project, :repository) }
it "includes the route" do
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index 84ad55e9f98..d56e14e0e0b 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -36,10 +36,10 @@ describe 'create_tokens' do
expect(keys).to all(match(HEX_KEY))
end
- it 'generates an RSA key for jws_private_key' do
+ it 'generates an RSA key for openid_connect_signing_key' do
create_tokens
- keys = secrets.values_at(:jws_private_key)
+ keys = secrets.values_at(:openid_connect_signing_key)
expect(keys.uniq).to eq(keys)
expect(keys).to all(match(RSA_KEY))
@@ -49,7 +49,7 @@ describe 'create_tokens' do
expect(self).to receive(:warn_missing_secret).with('secret_key_base')
expect(self).to receive(:warn_missing_secret).with('otp_key_base')
expect(self).to receive(:warn_missing_secret).with('db_key_base')
- expect(self).to receive(:warn_missing_secret).with('jws_private_key')
+ expect(self).to receive(:warn_missing_secret).with('openid_connect_signing_key')
create_tokens
end
@@ -61,7 +61,7 @@ describe 'create_tokens' do
expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base)
expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
- expect(new_secrets['jws_private_key']).to eq(secrets.jws_private_key)
+ expect(new_secrets['openid_connect_signing_key']).to eq(secrets.openid_connect_signing_key)
end
create_tokens
@@ -77,7 +77,7 @@ describe 'create_tokens' do
context 'when the other secrets all exist' do
before do
secrets.db_key_base = 'db_key_base'
- secrets.jws_private_key = 'jws_private_key'
+ secrets.openid_connect_signing_key = 'openid_connect_signing_key'
allow(File).to receive(:exist?).with('.secret').and_return(true)
allow(File).to receive(:read).with('.secret').and_return('file_key')
@@ -88,7 +88,7 @@ describe 'create_tokens' do
stub_env('SECRET_KEY_BASE', 'env_key')
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
- secrets.jws_private_key = 'jws_private_key'
+ secrets.openid_connect_signing_key = 'openid_connect_signing_key'
end
it 'does not issue a warning' do
@@ -114,7 +114,7 @@ describe 'create_tokens' do
before do
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
- secrets.jws_private_key = 'jws_private_key'
+ secrets.openid_connect_signing_key = 'openid_connect_signing_key'
end
it 'does not write any files' do
@@ -129,7 +129,7 @@ describe 'create_tokens' do
expect(secrets.secret_key_base).to eq('secret_key_base')
expect(secrets.otp_key_base).to eq('otp_key_base')
expect(secrets.db_key_base).to eq('db_key_base')
- expect(secrets.jws_private_key).to eq('jws_private_key')
+ expect(secrets.openid_connect_signing_key).to eq('openid_connect_signing_key')
end
it 'deletes the .secret file' do
@@ -153,7 +153,7 @@ describe 'create_tokens' do
expect(new_secrets['secret_key_base']).to eq('file_key')
expect(new_secrets['otp_key_base']).to eq('file_key')
expect(new_secrets['db_key_base']).to eq('db_key_base')
- expect(new_secrets['jws_private_key']).to eq('jws_private_key')
+ expect(new_secrets['openid_connect_signing_key']).to eq('openid_connect_signing_key')
end
create_tokens
diff --git a/spec/javascripts/abuse_reports_spec.js b/spec/javascripts/abuse_reports_spec.js
index 13cab81dd60..7f6b5873011 100644
--- a/spec/javascripts/abuse_reports_spec.js
+++ b/spec/javascripts/abuse_reports_spec.js
@@ -1,43 +1,41 @@
import '~/lib/utils/text_utility';
-import '~/abuse_reports';
-
-((global) => {
- describe('Abuse Reports', () => {
- const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
- const MAX_MESSAGE_LENGTH = 500;
-
- let $messages;
-
- const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
- const findMessage = searchText => $messages.filter(
- (index, element) => element.innerText.indexOf(searchText) > -1,
- ).first();
-
- preloadFixtures(FIXTURE);
-
- beforeEach(function () {
- loadFixtures(FIXTURE);
- this.abuseReports = new global.AbuseReports();
- $messages = $('.abuse-reports .message');
- });
-
- it('should truncate long messages', () => {
- const $longMessage = findMessage('LONG MESSAGE');
- expect($longMessage.data('original-message')).toEqual(jasmine.anything());
- assertMaxLength($longMessage);
- });
-
- it('should not truncate short messages', () => {
- const $shortMessage = findMessage('SHORT MESSAGE');
- expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
- });
-
- it('should allow clicking a truncated message to expand and collapse the full message', () => {
- const $longMessage = findMessage('LONG MESSAGE');
- $longMessage.click();
- expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
- $longMessage.click();
- assertMaxLength($longMessage);
- });
+import AbuseReports from '~/abuse_reports';
+
+describe('Abuse Reports', () => {
+ const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
+ const MAX_MESSAGE_LENGTH = 500;
+
+ let $messages;
+
+ const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
+ const findMessage = searchText => $messages.filter(
+ (index, element) => element.innerText.indexOf(searchText) > -1,
+ ).first();
+
+ preloadFixtures(FIXTURE);
+
+ beforeEach(function () {
+ loadFixtures(FIXTURE);
+ this.abuseReports = new AbuseReports();
+ $messages = $('.abuse-reports .message');
+ });
+
+ it('should truncate long messages', () => {
+ const $longMessage = findMessage('LONG MESSAGE');
+ expect($longMessage.data('original-message')).toEqual(jasmine.anything());
+ assertMaxLength($longMessage);
+ });
+
+ it('should not truncate short messages', () => {
+ const $shortMessage = findMessage('SHORT MESSAGE');
+ expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
+ });
+
+ it('should allow clicking a truncated message to expand and collapse the full message', () => {
+ const $longMessage = findMessage('LONG MESSAGE');
+ $longMessage.click();
+ expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
+ $longMessage.click();
+ assertMaxLength($longMessage);
});
-})(window.gl);
+});
diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js
index 46e072a8ebb..c93b7cc6cac 100644
--- a/spec/javascripts/ajax_loading_spinner_spec.js
+++ b/spec/javascripts/ajax_loading_spinner_spec.js
@@ -1,6 +1,6 @@
import 'jquery';
import 'jquery-ujs';
-import '~/ajax_loading_spinner';
+import AjaxLoadingSpinner from '~/ajax_loading_spinner';
describe('Ajax Loading Spinner', () => {
const fixtureTemplate = 'static/ajax_loading_spinner.html.raw';
@@ -8,7 +8,7 @@ describe('Ajax Loading Spinner', () => {
beforeEach(() => {
loadFixtures(fixtureTemplate);
- gl.AjaxLoadingSpinner.init();
+ AjaxLoadingSpinner.init();
});
it('change current icon with spinner icon and disable link while waiting ajax response', (done) => {
diff --git a/spec/javascripts/clusters_spec.js b/spec/javascripts/clusters_spec.js
new file mode 100644
index 00000000000..eb1cd6eb804
--- /dev/null
+++ b/spec/javascripts/clusters_spec.js
@@ -0,0 +1,79 @@
+import Clusters from '~/clusters';
+
+describe('Clusters', () => {
+ let cluster;
+ preloadFixtures('clusters/show_cluster.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('clusters/show_cluster.html.raw');
+ cluster = new Clusters();
+ });
+
+ describe('toggle', () => {
+ it('should update the button and the input field on click', () => {
+ cluster.toggleButton.click();
+
+ expect(
+ cluster.toggleButton.classList,
+ ).not.toContain('checked');
+
+ expect(
+ cluster.toggleInput.getAttribute('value'),
+ ).toEqual('false');
+ });
+ });
+
+ describe('updateContainer', () => {
+ describe('when creating cluster', () => {
+ it('should show the creating container', () => {
+ cluster.updateContainer('creating');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('when cluster is created', () => {
+ it('should show the success container', () => {
+ cluster.updateContainer('created');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('when cluster has error', () => {
+ it('should show the error container', () => {
+ cluster.updateContainer('errored', 'this is an error');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+
+ expect(
+ cluster.errorReasonContainer.textContent,
+ ).toContain('this is an error');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js
index ace95000468..e5a5e3293b9 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/javascripts/commits_spec.js
@@ -1,77 +1,73 @@
-/* global CommitsList */
-
import 'vendor/jquery.endless-scroll';
import '~/pager';
-import '~/commits';
-
-(() => {
- describe('Commits List', () => {
- beforeEach(() => {
- setFixtures(`
- <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
- <input id="commits-search">
- </form>
- <ol id="commits-list"></ol>
- `);
- });
+import CommitsList from '~/commits';
- it('should be defined', () => {
- expect(CommitsList).toBeDefined();
- });
+describe('Commits List', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
+ <input id="commits-search">
+ </form>
+ <ol id="commits-list"></ol>
+ `);
+ });
- describe('processCommits', () => {
- it('should join commit headers', () => {
- CommitsList.$contentList = $(`
- <div>
- <li class="commit-header" data-day="2016-09-20">
- <span class="day">20 Sep, 2016</span>
- <span class="commits-count">1 commit</span>
- </li>
- <li class="commit"></li>
- </div>
- `);
+ it('should be defined', () => {
+ expect(CommitsList).toBeDefined();
+ });
- const data = `
+ describe('processCommits', () => {
+ it('should join commit headers', () => {
+ CommitsList.$contentList = $(`
+ <div>
<li class="commit-header" data-day="2016-09-20">
<span class="day">20 Sep, 2016</span>
<span class="commits-count">1 commit</span>
</li>
<li class="commit"></li>
- `;
+ </div>
+ `);
- // The last commit header should be removed
- // since the previous one has the same data-day value.
- expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0);
- });
+ const data = `
+ <li class="commit-header" data-day="2016-09-20">
+ <span class="day">20 Sep, 2016</span>
+ <span class="commits-count">1 commit</span>
+ </li>
+ <li class="commit"></li>
+ `;
+
+ // The last commit header should be removed
+ // since the previous one has the same data-day value.
+ expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0);
});
+ });
- describe('on entering input', () => {
- let ajaxSpy;
+ describe('on entering input', () => {
+ let ajaxSpy;
- beforeEach(() => {
- CommitsList.init(25);
- CommitsList.searchField.val('');
+ beforeEach(() => {
+ CommitsList.init(25);
+ CommitsList.searchField.val('');
- spyOn(history, 'replaceState').and.stub();
- ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {
- req.success({
- data: '<li>Result</li>',
- });
+ spyOn(history, 'replaceState').and.stub();
+ ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {
+ req.success({
+ data: '<li>Result</li>',
});
});
+ });
- it('should save the last search string', () => {
- CommitsList.searchField.val('GitLab');
- CommitsList.filterResults();
- expect(ajaxSpy).toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('GitLab');
- });
+ it('should save the last search string', () => {
+ CommitsList.searchField.val('GitLab');
+ CommitsList.filterResults();
+ expect(ajaxSpy).toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('GitLab');
+ });
- it('should not make ajax call if the input does not change', () => {
- CommitsList.filterResults();
- expect(ajaxSpy).not.toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('');
- });
+ it('should not make ajax call if the input does not change', () => {
+ CommitsList.filterResults();
+ expect(ajaxSpy).not.toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('');
});
});
-})();
+});
diff --git a/spec/javascripts/cycle_analytics/banner_spec.js b/spec/javascripts/cycle_analytics/banner_spec.js
new file mode 100644
index 00000000000..fb6b7fee168
--- /dev/null
+++ b/spec/javascripts/cycle_analytics/banner_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import banner from '~/cycle_analytics/components/banner.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Cycle analytics banner', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(banner);
+ vm = mountComponent(Component, {
+ documentationLink: 'path',
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render cycle analytics information', () => {
+ expect(
+ vm.$el.querySelector('h4').textContent.trim(),
+ ).toEqual('Introducing Cycle Analytics');
+ expect(
+ vm.$el.querySelector('p').textContent.trim(),
+ ).toContain('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.');
+ expect(
+ vm.$el.querySelector('a').textContent.trim(),
+ ).toEqual('Read more');
+ expect(
+ vm.$el.querySelector('a').getAttribute('href'),
+ ).toEqual('path');
+ });
+
+ it('should emit an event when close button is clicked', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.js-ca-dismiss-button').click();
+
+ expect(vm.$emit).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/cycle_analytics/total_time_component_spec.js b/spec/javascripts/cycle_analytics/total_time_component_spec.js
new file mode 100644
index 00000000000..31b65fd1cde
--- /dev/null
+++ b/spec/javascripts/cycle_analytics/total_time_component_spec.js
@@ -0,0 +1,58 @@
+import Vue from 'vue';
+import component from '~/cycle_analytics/components/total_time_component.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Total time component', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('With data', () => {
+ it('should render information for days and hours', () => {
+ vm = mountComponent(Component, {
+ time: {
+ days: 3,
+ hours: 4,
+ },
+ });
+
+ expect(vm.$el.textContent.trim()).toEqual('3 days 4 hrs');
+ });
+
+ it('should render information for hours and minutes', () => {
+ vm = mountComponent(Component, {
+ time: {
+ hours: 4,
+ mins: 35,
+ },
+ });
+
+ expect(vm.$el.textContent.trim()).toEqual('4 hrs 35 mins');
+ });
+
+ it('should render information for seconds', () => {
+ vm = mountComponent(Component, {
+ time: {
+ seconds: 45,
+ },
+ });
+
+ expect(vm.$el.textContent.trim()).toEqual('45 s');
+ });
+ });
+
+ describe('Without data', () => {
+ it('should render no information', () => {
+ vm = mountComponent(Component);
+
+ expect(vm.$el.textContent.trim()).toEqual('--');
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
new file mode 100644
index 00000000000..5774f36f026
--- /dev/null
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Projects::ClustersController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :repository, namespace: namespace) }
+ let(:cluster) { project.create_cluster!(gcp_cluster_name: "gke-test-creation-1", gcp_project_id: 'gitlab-internal-153318', gcp_cluster_zone: 'us-central1-a', gcp_cluster_size: '1', project_namespace: 'aaa', gcp_machine_type: 'n1-standard-1')}
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('clusters/')
+ end
+
+ before do
+ sign_in(admin)
+ end
+
+ after do
+ remove_repository(project)
+ end
+
+ it 'clusters/show_cluster.html.raw' do |example|
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: cluster
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js
new file mode 100644
index 00000000000..060ffaa339b
--- /dev/null
+++ b/spec/javascripts/flash_spec.js
@@ -0,0 +1,269 @@
+import flash, {
+ createFlashEl,
+ createAction,
+ hideFlash,
+} from '~/flash';
+
+describe('Flash', () => {
+ describe('createFlashEl', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ });
+
+ afterEach(() => {
+ el.innerHTML = '';
+ });
+
+ it('creates flash element with type', () => {
+ el.innerHTML = createFlashEl('testing', 'alert');
+
+ expect(
+ el.querySelector('.flash-alert'),
+ ).not.toBeNull();
+ });
+
+ it('escapes text', () => {
+ el.innerHTML = createFlashEl('<script>alert("a");</script>', 'alert');
+
+ expect(
+ el.querySelector('.flash-text').textContent.trim(),
+ ).toBe('<script>alert("a");</script>');
+ });
+
+ it('adds container classes when inside content wrapper', () => {
+ el.innerHTML = createFlashEl('testing', 'alert', true);
+
+ expect(
+ el.querySelector('.flash-text').classList.contains('container-fluid'),
+ ).toBeTruthy();
+ expect(
+ el.querySelector('.flash-text').classList.contains('container-limited'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('hideFlash', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ el.className = 'js-testing';
+ });
+
+ it('sets transition style', () => {
+ hideFlash(el);
+
+ expect(
+ el.style.transition,
+ ).toBe('opacity 0.3s');
+ });
+
+ it('sets opacity style', () => {
+ hideFlash(el);
+
+ expect(
+ el.style.opacity,
+ ).toBe('0');
+ });
+
+ it('does not set styles when fadeTransition is false', () => {
+ hideFlash(el, false);
+
+ expect(
+ el.style.opacity,
+ ).toBe('');
+ expect(
+ el.style.transition,
+ ).toBe('');
+ });
+
+ it('removes element after transitionend', () => {
+ document.body.appendChild(el);
+
+ hideFlash(el);
+ el.dispatchEvent(new Event('transitionend'));
+
+ expect(
+ document.querySelector('.js-testing'),
+ ).toBeNull();
+ });
+
+ it('calls event listener callback once', () => {
+ spyOn(el, 'remove').and.callThrough();
+ document.body.appendChild(el);
+
+ hideFlash(el);
+
+ el.dispatchEvent(new Event('transitionend'));
+ el.dispatchEvent(new Event('transitionend'));
+
+ expect(
+ el.remove.calls.count(),
+ ).toBe(1);
+ });
+ });
+
+ describe('createAction', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ });
+
+ it('creates link with href', () => {
+ el.innerHTML = createAction({
+ href: 'testing',
+ title: 'test',
+ });
+
+ expect(
+ el.querySelector('.flash-action').href,
+ ).toContain('testing');
+ });
+
+ it('uses hash as href when no href is present', () => {
+ el.innerHTML = createAction({
+ title: 'test',
+ });
+
+ expect(
+ el.querySelector('.flash-action').href,
+ ).toContain('#');
+ });
+
+ it('adds role when no href is present', () => {
+ el.innerHTML = createAction({
+ title: 'test',
+ });
+
+ expect(
+ el.querySelector('.flash-action').getAttribute('role'),
+ ).toBe('button');
+ });
+
+ it('escapes the title text', () => {
+ el.innerHTML = createAction({
+ title: '<script>alert("a")</script>',
+ });
+
+ expect(
+ el.querySelector('.flash-action').textContent.trim(),
+ ).toBe('<script>alert("a")</script>');
+ });
+ });
+
+ describe('createFlash', () => {
+ describe('no flash-container', () => {
+ it('does not add to the DOM', () => {
+ const flashEl = flash('testing');
+
+ expect(
+ flashEl,
+ ).toBeNull();
+ expect(
+ document.querySelector('.flash-alert'),
+ ).toBeNull();
+ });
+ });
+
+ describe('with flash-container', () => {
+ beforeEach(() => {
+ document.body.innerHTML += `
+ <div class="content-wrapper js-content-wrapper">
+ <div class="flash-container"></div>
+ </div>
+ `;
+ });
+
+ afterEach(() => {
+ document.querySelector('.js-content-wrapper').remove();
+ });
+
+ it('adds flash element into container', () => {
+ flash('test');
+
+ expect(
+ document.querySelector('.flash-alert'),
+ ).not.toBeNull();
+ });
+
+ it('adds flash into specified parent', () => {
+ flash(
+ 'test',
+ 'alert',
+ document.querySelector('.content-wrapper'),
+ );
+
+ expect(
+ document.querySelector('.content-wrapper .flash-alert'),
+ ).not.toBeNull();
+ });
+
+ it('adds container classes when inside content-wrapper', () => {
+ flash('test');
+
+ expect(
+ document.querySelector('.flash-text').className,
+ ).toBe('flash-text container-fluid container-limited');
+ });
+
+ it('does not add container when outside of content-wrapper', () => {
+ document.querySelector('.content-wrapper').className = 'js-content-wrapper';
+ flash('test');
+
+ expect(
+ document.querySelector('.flash-text').className.trim(),
+ ).toBe('flash-text');
+ });
+
+ it('removes element after clicking', () => {
+ flash('test', 'alert', document, null, false);
+
+ document.querySelector('.flash-alert').click();
+
+ expect(
+ document.querySelector('.flash-alert'),
+ ).toBeNull();
+ });
+
+ describe('with actionConfig', () => {
+ it('adds action link', () => {
+ flash(
+ 'test',
+ 'alert',
+ document,
+ {
+ title: 'test',
+ },
+ );
+
+ expect(
+ document.querySelector('.flash-action'),
+ ).not.toBeNull();
+ });
+
+ it('calls actionConfig clickHandler on click', () => {
+ const actionConfig = {
+ title: 'test',
+ clickHandler: jasmine.createSpy('actionConfig'),
+ };
+
+ flash(
+ 'test',
+ 'alert',
+ document,
+ actionConfig,
+ );
+
+ document.querySelector('.flash-action').click();
+
+ expect(
+ actionConfig.clickHandler,
+ ).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index 0e01934d3a3..4751eb868a4 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -1,53 +1,48 @@
-/* eslint-disable space-before-function-paren, no-var */
-
import '~/header';
-import '~/lib/utils/text_utility';
-(function() {
- describe('Header', function() {
- var todosPendingCount = '.todos-count';
- var fixtureTemplate = 'issues/open-issue.html.raw';
+describe('Header', function () {
+ const todosPendingCount = '.todos-count';
+ const fixtureTemplate = 'issues/open-issue.html.raw';
- function isTodosCountHidden() {
- return $(todosPendingCount).hasClass('hidden');
- }
+ function isTodosCountHidden() {
+ return $(todosPendingCount).hasClass('hidden');
+ }
- function triggerToggle(newCount) {
- $(document).trigger('todo:toggle', newCount);
- }
+ function triggerToggle(newCount) {
+ $(document).trigger('todo:toggle', newCount);
+ }
- preloadFixtures(fixtureTemplate);
- beforeEach(function() {
- loadFixtures(fixtureTemplate);
- });
+ preloadFixtures(fixtureTemplate);
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ });
- it('should update todos-count after receiving the todo:toggle event', function() {
- triggerToggle(5);
- expect($(todosPendingCount).text()).toEqual('5');
- });
+ it('should update todos-count after receiving the todo:toggle event', () => {
+ triggerToggle('5');
+ expect($(todosPendingCount).text()).toEqual('5');
+ });
- it('should hide todos-count when it is 0', function() {
- triggerToggle(0);
- expect(isTodosCountHidden()).toEqual(true);
+ it('should hide todos-count when it is 0', () => {
+ triggerToggle('0');
+ expect(isTodosCountHidden()).toEqual(true);
+ });
+
+ it('should show todos-count when it is more than 0', () => {
+ triggerToggle('10');
+ expect(isTodosCountHidden()).toEqual(false);
+ });
+
+ describe('when todos-count is 1000', () => {
+ beforeEach(() => {
+ triggerToggle('1000');
});
- it('should show todos-count when it is more than 0', function() {
- triggerToggle(10);
+ it('should show todos-count', () => {
expect(isTodosCountHidden()).toEqual(false);
});
- describe('when todos-count is 1000', function() {
- beforeEach(function() {
- triggerToggle(1000);
- });
-
- it('should show todos-count', function() {
- expect(isTodosCountHidden()).toEqual(false);
- });
-
- it('should show 99+ for todos-count', function() {
- expect($(todosPendingCount).text()).toEqual('99+');
- });
+ it('should show 99+ for todos-count', () => {
+ expect($(todosPendingCount).text()).toEqual('99+');
});
});
-}).call(window);
+});
diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/javascripts/image_diff/helpers/badge_helper_spec.js
new file mode 100644
index 00000000000..fb9c7e59031
--- /dev/null
+++ b/spec/javascripts/image_diff/helpers/badge_helper_spec.js
@@ -0,0 +1,132 @@
+import * as badgeHelper from '~/image_diff/helpers/badge_helper';
+import * as mockData from '../mock_data';
+
+describe('badge helper', () => {
+ const { coordinate, noteId, badgeText, badgeNumber } = mockData;
+ let containerEl;
+ let buttonEl;
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ });
+
+ describe('createImageBadge', () => {
+ beforeEach(() => {
+ buttonEl = badgeHelper.createImageBadge(noteId, coordinate);
+ });
+
+ it('should create button', () => {
+ expect(buttonEl.tagName).toEqual('BUTTON');
+ expect(buttonEl.getAttribute('type')).toEqual('button');
+ });
+
+ it('should set disabled attribute', () => {
+ expect(buttonEl.hasAttribute('disabled')).toEqual(true);
+ });
+
+ it('should set noteId', () => {
+ expect(buttonEl.dataset.noteId).toEqual(noteId);
+ });
+
+ it('should set coordinate', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ describe('classNames', () => {
+ it('should set .js-image-badge by default', () => {
+ expect(buttonEl.className).toEqual('js-image-badge');
+ });
+
+ it('should add additional class names if parameter is passed', () => {
+ const classNames = ['first-class', 'second-class'];
+ buttonEl = badgeHelper.createImageBadge(noteId, coordinate, classNames);
+
+ expect(buttonEl.className).toEqual(classNames.concat('js-image-badge').join(' '));
+ });
+ });
+ });
+
+ describe('addImageBadge', () => {
+ beforeEach(() => {
+ badgeHelper.addImageBadge(containerEl, {
+ coordinate,
+ badgeText,
+ noteId,
+ });
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should appends button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ it('should set the badge text', () => {
+ expect(buttonEl.innerText).toEqual(badgeText);
+ });
+
+ it('should set the button coordinates', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ it('should set the button noteId', () => {
+ expect(buttonEl.dataset.noteId).toEqual(noteId);
+ });
+ });
+
+ describe('addImageCommentBadge', () => {
+ beforeEach(() => {
+ badgeHelper.addImageCommentBadge(containerEl, {
+ coordinate,
+ noteId,
+ });
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should append icon button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ it('should create icon comment button', () => {
+ const iconEl = buttonEl.querySelector('i');
+ expect(iconEl).toBeDefined();
+ expect(iconEl.classList.contains('fa')).toEqual(true);
+ expect(iconEl.classList.contains('fa-comment-o')).toEqual(true);
+ });
+
+ it('should have .image-comment-badge.inverted in button class', () => {
+ expect(buttonEl.classList.contains('image-comment-badge')).toEqual(true);
+ expect(buttonEl.classList.contains('inverted')).toEqual(true);
+ });
+ });
+
+ describe('addAvatarBadge', () => {
+ let avatarBadgeEl;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div id="${noteId}">
+ <div class="badge hidden">
+ </div>
+ </div>
+ `;
+
+ badgeHelper.addAvatarBadge(containerEl, {
+ detail: {
+ noteId,
+ badgeNumber,
+ },
+ });
+ avatarBadgeEl = containerEl.querySelector(`#${noteId} .badge`);
+ });
+
+ it('should update badge number', () => {
+ expect(avatarBadgeEl.innerText).toEqual(badgeNumber.toString());
+ });
+
+ it('should remove hidden class', () => {
+ expect(avatarBadgeEl.classList.contains('hidden')).toEqual(false);
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js b/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js
new file mode 100644
index 00000000000..a284b981d2a
--- /dev/null
+++ b/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js
@@ -0,0 +1,139 @@
+import * as commentIndicatorHelper from '~/image_diff/helpers/comment_indicator_helper';
+import * as mockData from '../mock_data';
+
+describe('commentIndicatorHelper', () => {
+ const { coordinate } = mockData;
+ let containerEl;
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ });
+
+ describe('addCommentIndicator', () => {
+ let buttonEl;
+
+ beforeEach(() => {
+ commentIndicatorHelper.addCommentIndicator(containerEl, coordinate);
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should append button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ describe('button', () => {
+ it('should set coordinate', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ it('should contain image-comment-dark svg', () => {
+ const svgEl = buttonEl.querySelector('svg');
+ expect(svgEl).toBeDefined();
+
+ const svgLink = svgEl.querySelector('use').getAttribute('xlink:href');
+ expect(svgLink.indexOf('image-comment-dark') !== -1).toEqual(true);
+ });
+ });
+ });
+
+ describe('removeCommentIndicator', () => {
+ it('should return removed false if there is no comment-indicator', () => {
+ const result = commentIndicatorHelper.removeCommentIndicator(containerEl);
+ expect(result.removed).toEqual(false);
+ });
+
+ describe('has comment indicator', () => {
+ let result;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div class="comment-indicator" style="left:${coordinate.x}px; top: ${coordinate.y}px;">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ `;
+ result = commentIndicatorHelper.removeCommentIndicator(containerEl);
+ });
+
+ it('should remove comment indicator', () => {
+ expect(containerEl.querySelector('.comment-indicator')).toBeNull();
+ });
+
+ it('should return removed true', () => {
+ expect(result.removed).toEqual(true);
+ });
+
+ it('should return indicator meta', () => {
+ expect(result.x).toEqual(coordinate.x);
+ expect(result.y).toEqual(coordinate.y);
+ expect(result.image).toBeDefined();
+ expect(result.image.width).toBeDefined();
+ expect(result.image.height).toBeDefined();
+ });
+ });
+ });
+
+ describe('showCommentIndicator', () => {
+ describe('commentIndicator exists', () => {
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <button class="comment-indicator"></button>
+ `;
+ commentIndicatorHelper.showCommentIndicator(containerEl, coordinate);
+ });
+
+ it('should set commentIndicator coordinates', () => {
+ const commentIndicatorEl = containerEl.querySelector('.comment-indicator');
+ expect(commentIndicatorEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(commentIndicatorEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+ });
+
+ describe('commentIndicator does not exist', () => {
+ beforeEach(() => {
+ commentIndicatorHelper.showCommentIndicator(containerEl, coordinate);
+ });
+
+ it('should addCommentIndicator', () => {
+ const buttonEl = containerEl.querySelector('.comment-indicator');
+ expect(buttonEl).toBeDefined();
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+ });
+ });
+
+ describe('commentIndicatorOnClick', () => {
+ let event;
+ let textAreaEl;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div class="diff-viewer">
+ <button></button>
+ <div class="note-container">
+ <textarea class="note-textarea"></textarea>
+ </div>
+ </div>
+ `;
+ textAreaEl = containerEl.querySelector('textarea');
+
+ event = {
+ stopPropagation: () => {},
+ currentTarget: containerEl.querySelector('button'),
+ };
+
+ spyOn(event, 'stopPropagation');
+ spyOn(textAreaEl, 'focus');
+ commentIndicatorHelper.commentIndicatorOnClick(event);
+ });
+
+ it('should stopPropagation', () => {
+ expect(event.stopPropagation).toHaveBeenCalled();
+ });
+
+ it('should focus textAreaEl', () => {
+ expect(textAreaEl.focus).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/helpers/dom_helper_spec.js b/spec/javascripts/image_diff/helpers/dom_helper_spec.js
new file mode 100644
index 00000000000..8dde924e8ae
--- /dev/null
+++ b/spec/javascripts/image_diff/helpers/dom_helper_spec.js
@@ -0,0 +1,118 @@
+import * as domHelper from '~/image_diff/helpers/dom_helper';
+import * as mockData from '../mock_data';
+
+describe('domHelper', () => {
+ const { imageMeta, badgeNumber } = mockData;
+
+ describe('setPositionDataAttribute', () => {
+ let containerEl;
+ let attributeAfterCall;
+ const position = {
+ myProperty: 'myProperty',
+ };
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ containerEl.dataset.position = JSON.stringify(position);
+ domHelper.setPositionDataAttribute(containerEl, imageMeta);
+ attributeAfterCall = JSON.parse(containerEl.dataset.position);
+ });
+
+ it('should set x, y, width, height', () => {
+ expect(attributeAfterCall.x).toEqual(imageMeta.x);
+ expect(attributeAfterCall.y).toEqual(imageMeta.y);
+ expect(attributeAfterCall.width).toEqual(imageMeta.width);
+ expect(attributeAfterCall.height).toEqual(imageMeta.height);
+ });
+
+ it('should not override other properties', () => {
+ expect(attributeAfterCall.myProperty).toEqual('myProperty');
+ });
+ });
+
+ describe('updateDiscussionAvatarBadgeNumber', () => {
+ let discussionEl;
+
+ beforeEach(() => {
+ discussionEl = document.createElement('div');
+ discussionEl.innerHTML = `
+ <a href="#" class="image-diff-avatar-link">
+ <div class="badge"></div>
+ </a>
+ `;
+ domHelper.updateDiscussionAvatarBadgeNumber(discussionEl, badgeNumber);
+ });
+
+ it('should update avatar badge number', () => {
+ expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString());
+ });
+ });
+
+ describe('updateDiscussionBadgeNumber', () => {
+ let discussionEl;
+
+ beforeEach(() => {
+ discussionEl = document.createElement('div');
+ discussionEl.innerHTML = `
+ <div class="badge"></div>
+ `;
+ domHelper.updateDiscussionBadgeNumber(discussionEl, badgeNumber);
+ });
+
+ it('should update discussion badge number', () => {
+ expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString());
+ });
+ });
+
+ describe('toggleCollapsed', () => {
+ let element;
+ let discussionNotesEl;
+
+ beforeEach(() => {
+ element = document.createElement('div');
+ element.innerHTML = `
+ <div class="discussion-notes">
+ <button></button>
+ <form class="discussion-form"></form>
+ </div>
+ `;
+ discussionNotesEl = element.querySelector('.discussion-notes');
+ });
+
+ describe('not collapsed', () => {
+ beforeEach(() => {
+ domHelper.toggleCollapsed({
+ currentTarget: element.querySelector('button'),
+ });
+ });
+
+ it('should add collapsed class', () => {
+ expect(discussionNotesEl.classList.contains('collapsed')).toEqual(true);
+ });
+
+ it('should force formEl to display none', () => {
+ const formEl = element.querySelector('.discussion-form');
+ expect(formEl.style.display).toEqual('none');
+ });
+ });
+
+ describe('collapsed', () => {
+ beforeEach(() => {
+ discussionNotesEl.classList.add('collapsed');
+
+ domHelper.toggleCollapsed({
+ currentTarget: element.querySelector('button'),
+ });
+ });
+
+ it('should remove collapsed class', () => {
+ expect(discussionNotesEl.classList.contains('collapsed')).toEqual(false);
+ });
+
+ it('should force formEl to display block', () => {
+ const formEl = element.querySelector('.discussion-form');
+ expect(formEl.style.display).toEqual('block');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/helpers/utils_helper_spec.js b/spec/javascripts/image_diff/helpers/utils_helper_spec.js
new file mode 100644
index 00000000000..56d77a05c4c
--- /dev/null
+++ b/spec/javascripts/image_diff/helpers/utils_helper_spec.js
@@ -0,0 +1,207 @@
+import * as utilsHelper from '~/image_diff/helpers/utils_helper';
+import ImageDiff from '~/image_diff/image_diff';
+import ReplacedImageDiff from '~/image_diff/replaced_image_diff';
+import ImageBadge from '~/image_diff/image_badge';
+import * as mockData from '../mock_data';
+
+describe('utilsHelper', () => {
+ const {
+ noteId,
+ discussionId,
+ image,
+ imageProperties,
+ imageMeta,
+ } = mockData;
+
+ describe('resizeCoordinatesToImageElement', () => {
+ let result;
+
+ beforeEach(() => {
+ result = utilsHelper.resizeCoordinatesToImageElement(image, imageMeta);
+ });
+
+ it('should return x based on widthRatio', () => {
+ expect(result.x).toEqual(imageMeta.x * 0.5);
+ });
+
+ it('should return y based on heightRatio', () => {
+ expect(result.y).toEqual(imageMeta.y * 0.5);
+ });
+
+ it('should return image width', () => {
+ expect(result.width).toEqual(image.width);
+ });
+
+ it('should return image height', () => {
+ expect(result.height).toEqual(image.height);
+ });
+ });
+
+ describe('generateBadgeFromDiscussionDOM', () => {
+ let discussionEl;
+ let result;
+
+ beforeEach(() => {
+ const imageFrameEl = document.createElement('div');
+ imageFrameEl.innerHTML = `
+ <img src="${gl.TEST_HOST}/image.png">
+ `;
+ discussionEl = document.createElement('div');
+ discussionEl.dataset.discussionId = discussionId;
+ discussionEl.innerHTML = `
+ <div class="note" id="${noteId}"></div>
+ `;
+ discussionEl.dataset.position = JSON.stringify(imageMeta);
+ result = utilsHelper.generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl);
+ });
+
+ it('should return actual image properties', () => {
+ const { actual } = result;
+ expect(actual.x).toEqual(imageMeta.x);
+ expect(actual.y).toEqual(imageMeta.y);
+ expect(actual.width).toEqual(imageMeta.width);
+ expect(actual.height).toEqual(imageMeta.height);
+ });
+
+ it('should return browser image properties', () => {
+ const { browser } = result;
+ expect(browser.x).toBeDefined();
+ expect(browser.y).toBeDefined();
+ expect(browser.width).toBeDefined();
+ expect(browser.height).toBeDefined();
+ });
+
+ it('should return instance of ImageBadge', () => {
+ expect(result instanceof ImageBadge).toEqual(true);
+ });
+
+ it('should return noteId', () => {
+ expect(result.noteId).toEqual(noteId);
+ });
+
+ it('should return discussionId', () => {
+ expect(result.discussionId).toEqual(discussionId);
+ });
+ });
+
+ describe('getTargetSelection', () => {
+ let containerEl;
+
+ beforeEach(() => {
+ containerEl = {
+ querySelector: () => imageProperties,
+ };
+ });
+
+ function generateEvent(offsetX, offsetY) {
+ return {
+ currentTarget: containerEl,
+ offsetX,
+ offsetY,
+ };
+ }
+
+ it('should return browser properties', () => {
+ const event = generateEvent(25, 25);
+ const result = utilsHelper.getTargetSelection(event);
+
+ const { browser } = result;
+ expect(browser.x).toEqual(event.offsetX);
+ expect(browser.y).toEqual(event.offsetY);
+ expect(browser.width).toEqual(imageProperties.width);
+ expect(browser.height).toEqual(imageProperties.height);
+ });
+
+ it('should return resized actual image properties', () => {
+ const event = generateEvent(50, 50);
+ const result = utilsHelper.getTargetSelection(event);
+
+ const { actual } = result;
+ expect(actual.x).toEqual(100);
+ expect(actual.y).toEqual(100);
+ expect(actual.width).toEqual(imageProperties.naturalWidth);
+ expect(actual.height).toEqual(imageProperties.naturalHeight);
+ });
+
+ describe('normalize coordinates', () => {
+ it('should return x = 0 if x < 0', () => {
+ const event = generateEvent(-5, 50);
+ const result = utilsHelper.getTargetSelection(event);
+ expect(result.browser.x).toEqual(0);
+ });
+
+ it('should return x = width if x > width', () => {
+ const event = generateEvent(1000, 50);
+ const result = utilsHelper.getTargetSelection(event);
+ expect(result.browser.x).toEqual(imageProperties.width);
+ });
+
+ it('should return y = 0 if y < 0', () => {
+ const event = generateEvent(50, -10);
+ const result = utilsHelper.getTargetSelection(event);
+ expect(result.browser.y).toEqual(0);
+ });
+
+ it('should return y = height if y > height', () => {
+ const event = generateEvent(50, 1000);
+ const result = utilsHelper.getTargetSelection(event);
+ expect(result.browser.y).toEqual(imageProperties.height);
+ });
+ });
+ });
+
+ describe('initImageDiff', () => {
+ let glCache;
+ let fileEl;
+
+ beforeEach(() => {
+ window.gl = window.gl || (window.gl = {});
+ glCache = window.gl;
+ window.gl.ImageFile = () => {};
+ fileEl = document.createElement('div');
+ fileEl.innerHTML = `
+ <div class="diff-file"></div>
+ `;
+
+ spyOn(ImageDiff.prototype, 'init').and.callFake(() => {});
+ spyOn(ReplacedImageDiff.prototype, 'init').and.callFake(() => {});
+ });
+
+ afterEach(() => {
+ window.gl = glCache;
+ });
+
+ it('should initialize gl.ImageFile', () => {
+ spyOn(window.gl, 'ImageFile');
+
+ utilsHelper.initImageDiff(fileEl, false, false);
+ expect(gl.ImageFile).toHaveBeenCalled();
+ });
+
+ it('should initialize ImageDiff if js-single-image', () => {
+ const diffFileEl = fileEl.querySelector('.diff-file');
+ diffFileEl.innerHTML = `
+ <div class="js-single-image">
+ </div>
+ `;
+
+ const imageDiff = utilsHelper.initImageDiff(fileEl, true, false);
+ expect(ImageDiff.prototype.init).toHaveBeenCalled();
+ expect(imageDiff.canCreateNote).toEqual(true);
+ expect(imageDiff.renderCommentBadge).toEqual(false);
+ });
+
+ it('should initialize ReplacedImageDiff if js-replaced-image', () => {
+ const diffFileEl = fileEl.querySelector('.diff-file');
+ diffFileEl.innerHTML = `
+ <div class="js-replaced-image">
+ </div>
+ `;
+
+ const replacedImageDiff = utilsHelper.initImageDiff(fileEl, false, true);
+ expect(ReplacedImageDiff.prototype.init).toHaveBeenCalled();
+ expect(replacedImageDiff.canCreateNote).toEqual(false);
+ expect(replacedImageDiff.renderCommentBadge).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/image_badge_spec.js b/spec/javascripts/image_diff/image_badge_spec.js
new file mode 100644
index 00000000000..87f98fc0926
--- /dev/null
+++ b/spec/javascripts/image_diff/image_badge_spec.js
@@ -0,0 +1,84 @@
+import ImageBadge from '~/image_diff/image_badge';
+import imageDiffHelper from '~/image_diff/helpers/index';
+import * as mockData from './mock_data';
+
+describe('ImageBadge', () => {
+ const { noteId, discussionId, imageMeta } = mockData;
+ const options = {
+ noteId,
+ discussionId,
+ };
+
+ it('should save actual property', () => {
+ const imageBadge = new ImageBadge(Object.assign({}, options, {
+ actual: imageMeta,
+ }));
+
+ const { actual } = imageBadge;
+ expect(actual.x).toEqual(imageMeta.x);
+ expect(actual.y).toEqual(imageMeta.y);
+ expect(actual.width).toEqual(imageMeta.width);
+ expect(actual.height).toEqual(imageMeta.height);
+ });
+
+ it('should save browser property', () => {
+ const imageBadge = new ImageBadge(Object.assign({}, options, {
+ browser: imageMeta,
+ }));
+
+ const { browser } = imageBadge;
+ expect(browser.x).toEqual(imageMeta.x);
+ expect(browser.y).toEqual(imageMeta.y);
+ expect(browser.width).toEqual(imageMeta.width);
+ expect(browser.height).toEqual(imageMeta.height);
+ });
+
+ it('should save noteId', () => {
+ const imageBadge = new ImageBadge(options);
+ expect(imageBadge.noteId).toEqual(noteId);
+ });
+
+ it('should save discussionId', () => {
+ const imageBadge = new ImageBadge(options);
+ expect(imageBadge.discussionId).toEqual(discussionId);
+ });
+
+ describe('default values', () => {
+ let imageBadge;
+
+ beforeEach(() => {
+ imageBadge = new ImageBadge(options);
+ });
+
+ it('should return defaultimageMeta if actual property is not provided', () => {
+ const { actual } = imageBadge;
+ expect(actual.x).toEqual(0);
+ expect(actual.y).toEqual(0);
+ expect(actual.width).toEqual(0);
+ expect(actual.height).toEqual(0);
+ });
+
+ it('should return defaultimageMeta if browser property is not provided', () => {
+ const { browser } = imageBadge;
+ expect(browser.x).toEqual(0);
+ expect(browser.y).toEqual(0);
+ expect(browser.width).toEqual(0);
+ expect(browser.height).toEqual(0);
+ });
+ });
+
+ describe('imageEl property is provided and not browser property', () => {
+ beforeEach(() => {
+ spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(true);
+ });
+
+ it('should generate browser property', () => {
+ const imageBadge = new ImageBadge(Object.assign({}, options, {
+ imageEl: document.createElement('img'),
+ }));
+
+ expect(imageDiffHelper.resizeCoordinatesToImageElement).toHaveBeenCalled();
+ expect(imageBadge.browser).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/image_diff_spec.js b/spec/javascripts/image_diff/image_diff_spec.js
new file mode 100644
index 00000000000..346282328c7
--- /dev/null
+++ b/spec/javascripts/image_diff/image_diff_spec.js
@@ -0,0 +1,361 @@
+import ImageDiff from '~/image_diff/image_diff';
+import * as imageUtility from '~/lib/utils/image_utility';
+import imageDiffHelper from '~/image_diff/helpers/index';
+import * as mockData from './mock_data';
+
+describe('ImageDiff', () => {
+ let element;
+ let imageDiff;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="element">
+ <div class="diff-file">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ <div class="comment-indicator"></div>
+ <div id="badge-1" class="badge">1</div>
+ <div id="badge-2" class="badge">2</div>
+ <div id="badge-3" class="badge">3</div>
+ </div>
+ <div class="note-container">
+ <div class="discussion-notes">
+ <div class="js-diff-notes-toggle"></div>
+ <div class="notes"></div>
+ </div>
+ <div class="discussion-notes">
+ <div class="js-diff-notes-toggle"></div>
+ <div class="notes"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ `);
+ element = document.getElementById('element');
+ });
+
+ describe('constructor', () => {
+ beforeEach(() => {
+ imageDiff = new ImageDiff(element, {
+ canCreateNote: true,
+ renderCommentBadge: true,
+ });
+ });
+
+ it('should set el', () => {
+ expect(imageDiff.el).toEqual(element);
+ });
+
+ it('should set canCreateNote', () => {
+ expect(imageDiff.canCreateNote).toEqual(true);
+ });
+
+ it('should set renderCommentBadge', () => {
+ expect(imageDiff.renderCommentBadge).toEqual(true);
+ });
+
+ it('should set $noteContainer', () => {
+ expect(imageDiff.$noteContainer[0]).toEqual(element.querySelector('.note-container'));
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ imageDiff = new ImageDiff(element);
+ });
+
+ it('should set canCreateNote as false', () => {
+ expect(imageDiff.canCreateNote).toEqual(false);
+ });
+
+ it('should set renderCommentBadge as false', () => {
+ expect(imageDiff.renderCommentBadge).toEqual(false);
+ });
+ });
+ });
+
+ describe('init', () => {
+ beforeEach(() => {
+ spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.init();
+ });
+
+ it('should set imageFrameEl', () => {
+ expect(imageDiff.imageFrameEl).toEqual(element.querySelector('.diff-file .js-image-frame'));
+ });
+
+ it('should set imageEl', () => {
+ expect(imageDiff.imageEl).toEqual(element.querySelector('.diff-file .js-image-frame img'));
+ });
+
+ it('should call bindEvents', () => {
+ expect(imageDiff.bindEvents).toHaveBeenCalled();
+ });
+ });
+
+ describe('bindEvents', () => {
+ let imageEl;
+
+ beforeEach(() => {
+ spyOn(imageDiffHelper, 'toggleCollapsed').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'commentIndicatorOnClick').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'removeCommentIndicator').and.callFake(() => {});
+ spyOn(ImageDiff.prototype, 'imageClicked').and.callFake(() => {});
+ spyOn(ImageDiff.prototype, 'addBadge').and.callFake(() => {});
+ spyOn(ImageDiff.prototype, 'removeBadge').and.callFake(() => {});
+ spyOn(ImageDiff.prototype, 'renderBadges').and.callFake(() => {});
+ imageEl = element.querySelector('.diff-file .js-image-frame img');
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should register click event delegation to js-diff-notes-toggle', () => {
+ element.querySelector('.js-diff-notes-toggle').click();
+ expect(imageDiffHelper.toggleCollapsed).toHaveBeenCalled();
+ });
+
+ it('should register click event delegation to comment-indicator', () => {
+ element.querySelector('.comment-indicator').click();
+ expect(imageDiffHelper.commentIndicatorOnClick).toHaveBeenCalled();
+ });
+ });
+
+ describe('image loaded', () => {
+ beforeEach(() => {
+ spyOn(imageUtility, 'isImageLoaded').and.returnValue(true);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ });
+
+ it('should renderBadges', () => {});
+ });
+
+ describe('image not loaded', () => {
+ beforeEach(() => {
+ spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should registers load eventListener', () => {
+ const loadEvent = new Event('load');
+ imageEl.dispatchEvent(loadEvent);
+ expect(imageDiff.renderBadges).toHaveBeenCalled();
+ });
+ });
+
+ describe('canCreateNote', () => {
+ beforeEach(() => {
+ spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ imageDiff = new ImageDiff(element, {
+ canCreateNote: true,
+ });
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should register click.imageDiff event', () => {
+ const event = new CustomEvent('click.imageDiff');
+ element.dispatchEvent(event);
+ expect(imageDiff.imageClicked).toHaveBeenCalled();
+ });
+
+ it('should register blur.imageDiff event', () => {
+ const event = new CustomEvent('blur.imageDiff');
+ element.dispatchEvent(event);
+ expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled();
+ });
+
+ it('should register addBadge.imageDiff event', () => {
+ const event = new CustomEvent('addBadge.imageDiff');
+ element.dispatchEvent(event);
+ expect(imageDiff.addBadge).toHaveBeenCalled();
+ });
+
+ it('should register removeBadge.imageDiff event', () => {
+ const event = new CustomEvent('removeBadge.imageDiff');
+ element.dispatchEvent(event);
+ expect(imageDiff.removeBadge).toHaveBeenCalled();
+ });
+ });
+
+ describe('canCreateNote is false', () => {
+ beforeEach(() => {
+ spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should not register click.imageDiff event', () => {
+ const event = new CustomEvent('click.imageDiff');
+ element.dispatchEvent(event);
+ expect(imageDiff.imageClicked).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('imageClicked', () => {
+ beforeEach(() => {
+ spyOn(imageDiffHelper, 'getTargetSelection').and.returnValue({
+ actual: {},
+ browser: {},
+ });
+ spyOn(imageDiffHelper, 'setPositionDataAttribute').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageClicked({
+ detail: {
+ currentTarget: {},
+ },
+ });
+ });
+
+ it('should call getTargetSelection', () => {
+ expect(imageDiffHelper.getTargetSelection).toHaveBeenCalled();
+ });
+
+ it('should call setPositionDataAttribute', () => {
+ expect(imageDiffHelper.setPositionDataAttribute).toHaveBeenCalled();
+ });
+
+ it('should call showCommentIndicator', () => {
+ expect(imageDiffHelper.showCommentIndicator).toHaveBeenCalled();
+ });
+ });
+
+ describe('renderBadges', () => {
+ beforeEach(() => {
+ spyOn(ImageDiff.prototype, 'renderBadge').and.callFake(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.renderBadges();
+ });
+
+ it('should call renderBadge for each discussionEl', () => {
+ const discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes');
+ expect(imageDiff.renderBadge.calls.count()).toEqual(discussionEls.length);
+ });
+ });
+
+ describe('renderBadge', () => {
+ let discussionEls;
+
+ beforeEach(() => {
+ spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'addImageCommentBadge').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').and.returnValue({
+ browser: {},
+ noteId: 'noteId',
+ });
+ discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes');
+ imageDiff = new ImageDiff(element);
+ imageDiff.renderBadge(discussionEls[0], 0);
+ });
+
+ it('should populate imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(1);
+ });
+
+ describe('renderCommentBadge', () => {
+ beforeEach(() => {
+ imageDiff.renderCommentBadge = true;
+ imageDiff.renderBadge(discussionEls[0], 0);
+ });
+
+ it('should call addImageCommentBadge', () => {
+ expect(imageDiffHelper.addImageCommentBadge).toHaveBeenCalled();
+ });
+ });
+
+ describe('renderCommentBadge is false', () => {
+ it('should call addImageBadge', () => {
+ expect(imageDiffHelper.addImageBadge).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addBadge', () => {
+ beforeEach(() => {
+ spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'addAvatarBadge').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
+ imageDiff.addBadge({
+ detail: {
+ x: 0,
+ y: 1,
+ width: 25,
+ height: 50,
+ noteId: 'noteId',
+ discussionId: 'discussionId',
+ },
+ });
+ });
+
+ it('should add imageBadge to imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(1);
+ });
+
+ it('should call addImageBadge', () => {
+ expect(imageDiffHelper.addImageBadge).toHaveBeenCalled();
+ });
+
+ it('should call addAvatarBadge', () => {
+ expect(imageDiffHelper.addAvatarBadge).toHaveBeenCalled();
+ });
+
+ it('should call updateDiscussionBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled();
+ });
+ });
+
+ describe('removeBadge', () => {
+ beforeEach(() => {
+ const { imageMeta } = mockData;
+
+ spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').and.callFake(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageBadges = [imageMeta, imageMeta, imageMeta];
+ imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
+ imageDiff.removeBadge({
+ detail: {
+ badgeNumber: 2,
+ },
+ });
+ });
+
+ describe('cascade badge count', () => {
+ it('should update next imageBadgeEl value', () => {
+ const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge');
+ expect(imageBadgeEls[0].innerText).toEqual('1');
+ expect(imageBadgeEls[1].innerText).toEqual('2');
+ expect(imageBadgeEls.length).toEqual(2);
+ });
+
+ it('should call updateDiscussionBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled();
+ });
+
+ it('should call updateDiscussionAvatarBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionAvatarBadgeNumber).toHaveBeenCalled();
+ });
+ });
+
+ it('should remove badge from imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(2);
+ });
+
+ it('should remove imageBadgeEl', () => {
+ expect(imageDiff.imageFrameEl.querySelector('#badge-2')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/init_discussion_tab_spec.js b/spec/javascripts/image_diff/init_discussion_tab_spec.js
new file mode 100644
index 00000000000..7c447d6f70d
--- /dev/null
+++ b/spec/javascripts/image_diff/init_discussion_tab_spec.js
@@ -0,0 +1,37 @@
+import initDiscussionTab from '~/image_diff/init_discussion_tab';
+import imageDiffHelper from '~/image_diff/helpers/index';
+
+describe('initDiscussionTab', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <div class="timeline-content">
+ <div class="diff-file js-image-file"></div>
+ <div class="diff-file js-image-file"></div>
+ </div>
+ `);
+ });
+
+ it('should pass canCreateNote as false to initImageDiff', (done) => {
+ spyOn(imageDiffHelper, 'initImageDiff').and.callFake((diffFileEl, canCreateNote) => {
+ expect(canCreateNote).toEqual(false);
+ done();
+ });
+
+ initDiscussionTab();
+ });
+
+ it('should pass renderCommentBadge as true to initImageDiff', (done) => {
+ spyOn(imageDiffHelper, 'initImageDiff').and.callFake((diffFileEl, canCreateNote, renderCommentBadge) => {
+ expect(renderCommentBadge).toEqual(true);
+ done();
+ });
+
+ initDiscussionTab();
+ });
+
+ it('should call initImageDiff for each diffFileEls', () => {
+ spyOn(imageDiffHelper, 'initImageDiff').and.callFake(() => {});
+ initDiscussionTab();
+ expect(imageDiffHelper.initImageDiff.calls.count()).toEqual(2);
+ });
+});
diff --git a/spec/javascripts/image_diff/mock_data.js b/spec/javascripts/image_diff/mock_data.js
new file mode 100644
index 00000000000..a0d1732dd0a
--- /dev/null
+++ b/spec/javascripts/image_diff/mock_data.js
@@ -0,0 +1,28 @@
+export const noteId = 'noteId';
+export const discussionId = 'discussionId';
+export const badgeText = 'badgeText';
+export const badgeNumber = 5;
+
+export const coordinate = {
+ x: 100,
+ y: 100,
+};
+
+export const image = {
+ width: 100,
+ height: 100,
+};
+
+export const imageProperties = {
+ width: image.width,
+ height: image.height,
+ naturalWidth: image.width * 2,
+ naturalHeight: image.height * 2,
+};
+
+export const imageMeta = {
+ x: coordinate.x,
+ y: coordinate.y,
+ width: imageProperties.naturalWidth,
+ height: imageProperties.naturalHeight,
+};
diff --git a/spec/javascripts/image_diff/replaced_image_diff_spec.js b/spec/javascripts/image_diff/replaced_image_diff_spec.js
new file mode 100644
index 00000000000..5f8cd7c531a
--- /dev/null
+++ b/spec/javascripts/image_diff/replaced_image_diff_spec.js
@@ -0,0 +1,312 @@
+import ReplacedImageDiff from '~/image_diff/replaced_image_diff';
+import ImageDiff from '~/image_diff/image_diff';
+import { viewTypes } from '~/image_diff/view_types';
+import imageDiffHelper from '~/image_diff/helpers/index';
+
+describe('ReplacedImageDiff', () => {
+ let element;
+ let replacedImageDiff;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="element">
+ <div class="two-up">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="swipe">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="onion-skin">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="view-modes-menu">
+ <div class="two-up">2-up</div>
+ <div class="swipe">Swipe</div>
+ <div class="onion-skin">Onion skin</div>
+ </div>
+ </div>
+ `);
+ element = document.getElementById('element');
+ });
+
+ function setupImageFrameEls() {
+ replacedImageDiff.imageFrameEls = [];
+ replacedImageDiff.imageFrameEls[viewTypes.TWO_UP] = element.querySelector('.two-up .js-image-frame');
+ replacedImageDiff.imageFrameEls[viewTypes.SWIPE] = element.querySelector('.swipe .js-image-frame');
+ replacedImageDiff.imageFrameEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin .js-image-frame');
+ }
+
+ function setupViewModesEls() {
+ replacedImageDiff.viewModesEls = [];
+ replacedImageDiff.viewModesEls[viewTypes.TWO_UP] = element.querySelector('.view-modes-menu .two-up');
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE] = element.querySelector('.view-modes-menu .swipe');
+ replacedImageDiff.viewModesEls[viewTypes.ONION_SKIN] = element.querySelector('.view-modes-menu .onion-skin');
+ }
+
+ function setupImageEls() {
+ replacedImageDiff.imageEls = [];
+ replacedImageDiff.imageEls[viewTypes.TWO_UP] = element.querySelector('.two-up img');
+ replacedImageDiff.imageEls[viewTypes.SWIPE] = element.querySelector('.swipe img');
+ replacedImageDiff.imageEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin img');
+ }
+
+ it('should extend ImageDiff', () => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ expect(replacedImageDiff instanceof ImageDiff).toEqual(true);
+ });
+
+ describe('init', () => {
+ beforeEach(() => {
+ spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {});
+ spyOn(ReplacedImageDiff.prototype, 'generateImageEls').and.callFake(() => {});
+
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.init();
+ });
+
+ it('should set imageFrameEls', () => {
+ const { imageFrameEls } = replacedImageDiff;
+ expect(imageFrameEls).toBeDefined();
+ expect(imageFrameEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up .js-image-frame'));
+ expect(imageFrameEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe .js-image-frame'));
+ expect(imageFrameEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin .js-image-frame'));
+ });
+
+ it('should set viewModesEls', () => {
+ const { viewModesEls } = replacedImageDiff;
+ expect(viewModesEls).toBeDefined();
+ expect(viewModesEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.view-modes-menu .two-up'));
+ expect(viewModesEls[viewTypes.SWIPE]).toEqual(element.querySelector('.view-modes-menu .swipe'));
+ expect(viewModesEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.view-modes-menu .onion-skin'));
+ });
+
+ it('should generateImageEls', () => {
+ expect(ReplacedImageDiff.prototype.generateImageEls).toHaveBeenCalled();
+ });
+
+ it('should bindEvents', () => {
+ expect(ReplacedImageDiff.prototype.bindEvents).toHaveBeenCalled();
+ });
+
+ describe('currentView', () => {
+ it('should set currentView', () => {
+ replacedImageDiff.init(viewTypes.ONION_SKIN);
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN);
+ });
+
+ it('should default to viewTypes.TWO_UP', () => {
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.TWO_UP);
+ });
+ });
+ });
+
+ describe('generateImageEls', () => {
+ beforeEach(() => {
+ spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {});
+
+ replacedImageDiff = new ReplacedImageDiff(element, {
+ canCreateNote: false,
+ renderCommentBadge: false,
+ });
+
+ setupImageFrameEls();
+ });
+
+ it('should set imageEls', () => {
+ replacedImageDiff.generateImageEls();
+ const { imageEls } = replacedImageDiff;
+ expect(imageEls).toBeDefined();
+ expect(imageEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up img'));
+ expect(imageEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe img'));
+ expect(imageEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin img'));
+ });
+ });
+
+ describe('bindEvents', () => {
+ beforeEach(() => {
+ spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {});
+ replacedImageDiff = new ReplacedImageDiff(element);
+
+ setupViewModesEls();
+ });
+
+ it('should call super.bindEvents', () => {
+ replacedImageDiff.bindEvents();
+ expect(ImageDiff.prototype.bindEvents).toHaveBeenCalled();
+ });
+
+ it('should register click eventlistener to 2-up view mode', (done) => {
+ spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => {
+ expect(viewMode).toEqual(viewTypes.TWO_UP);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.TWO_UP].click();
+ });
+
+ it('should register click eventlistener to swipe view mode', (done) => {
+ spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => {
+ expect(viewMode).toEqual(viewTypes.SWIPE);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
+ });
+
+ it('should register click eventlistener to onion skin view mode', (done) => {
+ spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => {
+ expect(viewMode).toEqual(viewTypes.SWIPE);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
+ });
+ });
+
+ describe('getters', () => {
+ describe('imageEl', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.currentView = viewTypes.TWO_UP;
+ setupImageEls();
+ });
+
+ it('should return imageEl based on currentView', () => {
+ expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.two-up img'));
+
+ replacedImageDiff.currentView = viewTypes.SWIPE;
+ expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.swipe img'));
+ });
+ });
+
+ describe('imageFrameEl', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.currentView = viewTypes.TWO_UP;
+ setupImageFrameEls();
+ });
+
+ it('should return imageFrameEl based on currentView', () => {
+ expect(replacedImageDiff.imageFrameEl).toEqual(element.querySelector('.two-up .js-image-frame'));
+
+ replacedImageDiff.currentView = viewTypes.ONION_SKIN;
+ expect(replacedImageDiff.imageFrameEl).toEqual(element.querySelector('.onion-skin .js-image-frame'));
+ });
+ });
+ });
+
+ describe('changeView', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ spyOn(imageDiffHelper, 'removeCommentIndicator').and.returnValue({
+ removed: false,
+ });
+ setupImageFrameEls();
+ });
+
+ describe('invalid viewType', () => {
+ beforeEach(() => {
+ replacedImageDiff.changeView('some-view-name');
+ });
+
+ it('should not call removeCommentIndicator', () => {
+ expect(imageDiffHelper.removeCommentIndicator).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('valid viewType', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
+ spyOn(ReplacedImageDiff.prototype, 'renderNewView').and.callFake(() => {});
+ replacedImageDiff.changeView(viewTypes.ONION_SKIN);
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
+ it('should call removeCommentIndicator', () => {
+ expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled();
+ });
+
+ it('should update currentView to newView', () => {
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN);
+ });
+
+ it('should clear imageBadges', () => {
+ expect(replacedImageDiff.imageBadges.length).toEqual(0);
+ });
+
+ it('should call renderNewView', () => {
+ jasmine.clock().tick(251);
+ expect(replacedImageDiff.renderNewView).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('renderNewView', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ });
+
+ it('should call renderBadges', () => {
+ spyOn(ReplacedImageDiff.prototype, 'renderBadges').and.callFake(() => {});
+
+ replacedImageDiff.renderNewView({
+ removed: false,
+ });
+
+ expect(replacedImageDiff.renderBadges).toHaveBeenCalled();
+ });
+
+ describe('removeIndicator', () => {
+ const indicator = {
+ removed: true,
+ x: 0,
+ y: 1,
+ image: {
+ width: 50,
+ height: 100,
+ },
+ };
+
+ beforeEach(() => {
+ setupImageEls();
+ setupImageFrameEls();
+ });
+
+ it('should pass showCommentIndicator normalized indicator values', (done) => {
+ spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {});
+ spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.callFake((imageEl, meta) => {
+ expect(meta.x).toEqual(indicator.x);
+ expect(meta.y).toEqual(indicator.y);
+ expect(meta.width).toEqual(indicator.image.width);
+ expect(meta.height).toEqual(indicator.image.height);
+ done();
+ });
+ replacedImageDiff.renderNewView(indicator);
+ });
+
+ it('should call showCommentIndicator', (done) => {
+ const normalized = {
+ normalized: true,
+ };
+ spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(normalized);
+ spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake((imageFrameEl, normalizedIndicator) => {
+ expect(normalizedIndicator).toEqual(normalized);
+ done();
+ });
+ replacedImageDiff.renderNewView(indicator);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/view_types_spec.js b/spec/javascripts/image_diff/view_types_spec.js
new file mode 100644
index 00000000000..e9639f46497
--- /dev/null
+++ b/spec/javascripts/image_diff/view_types_spec.js
@@ -0,0 +1,24 @@
+import { viewTypes, isValidViewType } from '~/image_diff/view_types';
+
+describe('viewTypes', () => {
+ describe('isValidViewType', () => {
+ it('should return true for TWO_UP', () => {
+ expect(isValidViewType(viewTypes.TWO_UP)).toEqual(true);
+ });
+
+ it('should return true for SWIPE', () => {
+ expect(isValidViewType(viewTypes.SWIPE)).toEqual(true);
+ });
+
+ it('should return true for ONION_SKIN', () => {
+ expect(isValidViewType(viewTypes.ONION_SKIN)).toEqual(true);
+ });
+
+ it('should return false for non view types', () => {
+ expect(isValidViewType('some-view-type')).toEqual(false);
+ expect(isValidViewType(null)).toEqual(false);
+ expect(isValidViewType(undefined)).toEqual(false);
+ expect(isValidViewType('')).toEqual(false);
+ });
+ });
+});
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
index 3daeb91b1e2..9033eb9ce02 100644
--- a/spec/javascripts/integrations/integration_settings_form_spec.js
+++ b/spec/javascripts/integrations/integration_settings_form_spec.js
@@ -138,9 +138,9 @@ describe('IntegrationSettingsForm', () => {
deferred.resolve({ error: true, message: errorMessage, service_response: 'some error' });
const $flashContainer = $('.flash-container');
- expect($flashContainer.find('.flash-text').text()).toEqual('Test failed. some error');
+ expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error');
expect($flashContainer.find('.flash-action')).toBeDefined();
- expect($flashContainer.find('.flash-action').text()).toEqual('Save anyway');
+ expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway');
});
it('should submit form if ajax request responds without any error in test', () => {
@@ -168,7 +168,7 @@ describe('IntegrationSettingsForm', () => {
expect($flashAction).toBeDefined();
spyOn(integrationSettingsForm.$form, 'submit');
- $flashAction.trigger('click');
+ $flashAction.get(0).click();
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
});
@@ -181,7 +181,7 @@ describe('IntegrationSettingsForm', () => {
deferred.reject();
- expect($('.flash-container .flash-text').text()).toEqual(errorMessage);
+ expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage);
});
it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => {
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/job_spec.js
index d5b0f23e7b7..5e67911d338 100644
--- a/spec/javascripts/build_spec.js
+++ b/spec/javascripts/job_spec.js
@@ -1,13 +1,11 @@
-/* eslint-disable no-new */
-/* global Build */
import { bytesToKiB } from '~/lib/utils/number_utils';
import '~/lib/utils/datetime_utility';
import '~/lib/utils/url_utility';
-import '~/build';
+import Job from '~/job';
import '~/breakpoints';
-describe('Build', () => {
- const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
+describe('Job', () => {
+ const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
preloadFixtures('builds/build-with-artifacts.html.raw');
@@ -26,14 +24,14 @@ describe('Build', () => {
describe('setup', () => {
beforeEach(function () {
- this.build = new Build();
+ this.job = new Job();
});
it('copies build options', function () {
- expect(this.build.pageUrl).toBe(BUILD_URL);
- expect(this.build.buildStatus).toBe('success');
- expect(this.build.buildStage).toBe('test');
- expect(this.build.state).toBe('');
+ expect(this.job.pageUrl).toBe(JOB_URL);
+ expect(this.job.buildStatus).toBe('success');
+ expect(this.job.buildStage).toBe('test');
+ expect(this.job.state).toBe('');
});
it('only shows the jobs matching the current stage', () => {
@@ -87,15 +85,15 @@ describe('Build', () => {
complete: true,
});
- this.build = new Build();
+ this.job = new Job();
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
- expect(this.build.state).toBe('newstate');
+ expect(this.job.state).toBe('newstate');
jasmine.clock().tick(4001);
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
- expect(this.build.state).toBe('finalstate');
+ expect(this.job.state).toBe('finalstate');
});
it('replaces the entire build trace', () => {
@@ -122,7 +120,7 @@ describe('Build', () => {
append: false,
});
- this.build = new Build();
+ this.job = new Job();
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
@@ -148,7 +146,7 @@ describe('Build', () => {
total: 100,
});
- this.build = new Build();
+ this.job = new Job();
expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
});
@@ -167,7 +165,7 @@ describe('Build', () => {
total: 100,
});
- this.build = new Build();
+ this.job = new Job();
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
@@ -193,7 +191,7 @@ describe('Build', () => {
deferred2.resolve();
- this.build = new Build();
+ this.job = new Job();
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
@@ -227,7 +225,7 @@ describe('Build', () => {
total: 100,
});
- this.build = new Build();
+ this.job = new Job();
expect(
document.querySelector('.js-raw-link').textContent.trim(),
@@ -249,7 +247,7 @@ describe('Build', () => {
total: 100,
});
- this.build = new Build();
+ this.job = new Job();
expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
});
@@ -270,7 +268,7 @@ describe('Build', () => {
total: 100,
});
- this.build = new Build();
+ this.job = new Job();
});
it('should render trace controls', () => {
@@ -293,11 +291,12 @@ describe('Build', () => {
describe('getBuildTrace', () => {
it('should request build trace with state parameter', (done) => {
spyOn(jQuery, 'ajax').and.callThrough();
- new Build();
+ // eslint-disable-next-line no-new
+ new Job();
setTimeout(() => {
expect(jQuery.ajax).toHaveBeenCalledWith(
- { url: `${BUILD_URL}/trace.json`, data: { state: '' } },
+ { url: `${JOB_URL}/trace.json`, data: { state: '' } },
);
done();
}, 0);
diff --git a/spec/javascripts/lib/utils/image_utility_spec.js b/spec/javascripts/lib/utils/image_utility_spec.js
new file mode 100644
index 00000000000..75addfcc833
--- /dev/null
+++ b/spec/javascripts/lib/utils/image_utility_spec.js
@@ -0,0 +1,32 @@
+import * as imageUtility from '~/lib/utils/image_utility';
+
+describe('imageUtility', () => {
+ describe('isImageLoaded', () => {
+ it('should return false when image.complete is false', () => {
+ const element = {
+ complete: false,
+ naturalHeight: 100,
+ };
+
+ expect(imageUtility.isImageLoaded(element)).toEqual(false);
+ });
+
+ it('should return false when naturalHeight = 0', () => {
+ const element = {
+ complete: true,
+ naturalHeight: 0,
+ };
+
+ expect(imageUtility.isImageLoaded(element)).toEqual(false);
+ });
+
+ it('should return true when image.complete and naturalHeight != 0', () => {
+ const element = {
+ complete: true,
+ naturalHeight: 100,
+ };
+
+ expect(imageUtility.isImageLoaded(element)).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index f1a975ba962..829b3ef5735 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -1,4 +1,4 @@
-import '~/lib/utils/text_utility';
+import { highCountTrim } from '~/lib/utils/text_utility';
describe('text_utility', () => {
describe('gl.text.getTextWidth', () => {
@@ -35,14 +35,14 @@ describe('text_utility', () => {
});
});
- describe('gl.text.highCountTrim', () => {
+ describe('highCountTrim', () => {
it('returns 99+ for count >= 100', () => {
- expect(gl.text.highCountTrim(105)).toBe('99+');
- expect(gl.text.highCountTrim(100)).toBe('99+');
+ expect(highCountTrim(105)).toBe('99+');
+ expect(highCountTrim(100)).toBe('99+');
});
it('returns exact number for count < 100', () => {
- expect(gl.text.highCountTrim(45)).toBe(45);
+ expect(highCountTrim(45)).toBe(45);
});
});
diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js
index c2ff38ffab9..dea42d755d4 100644
--- a/spec/javascripts/monitoring/graph/deployment_spec.js
+++ b/spec/javascripts/monitoring/graph/deployment_spec.js
@@ -21,6 +21,7 @@ describe('MonitoringDeployment', () => {
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
+ graphWidth: 440,
graphHeight: 300,
graphHeightOffset: 120,
});
@@ -36,6 +37,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -49,6 +51,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -62,6 +65,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -75,6 +79,7 @@ describe('MonitoringDeployment', () => {
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
+ graphWidth: 440,
graphHeight: 300,
graphHeightOffset: 120,
});
@@ -82,12 +87,29 @@ describe('MonitoringDeployment', () => {
expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull();
});
+ it('positions the flag to the left when the xPos is too far right', () => {
+ reducedDeploymentData[0].showDeploymentFlag = false;
+ reducedDeploymentData[0].xPos = 250;
+ const component = createComponent({
+ showDeployInfo: true,
+ deploymentData: reducedDeploymentData,
+ graphWidth: 440,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ });
+
+ expect(
+ component.positionFlag(reducedDeploymentData[0]),
+ ).toBeLessThan(0);
+ });
+
it('shows the deployment flag', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -102,6 +124,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -115,6 +138,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
@@ -127,6 +151,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
+ graphWidth: 440,
graphHeightOffset: 120,
});
diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js
index 14794cbfd50..8ee1171419d 100644
--- a/spec/javascripts/monitoring/graph/flag_spec.js
+++ b/spec/javascripts/monitoring/graph/flag_spec.js
@@ -14,19 +14,22 @@ function getCoordinate(component, selector, coordinate) {
return parseInt(coordinateVal, 10);
}
+const defaultValuesComponent = {
+ currentXCoordinate: 200,
+ currentYCoordinate: 100,
+ currentFlagPosition: 100,
+ currentData: {
+ time: new Date('2017-06-04T18:17:33.501Z'),
+ value: '1.49609375',
+ },
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ showFlagContent: true,
+};
+
describe('GraphFlag', () => {
it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => {
- const component = createComponent({
- currentXCoordinate: 200,
- currentYCoordinate: 100,
- currentFlagPosition: 100,
- currentData: {
- time: new Date('2017-06-04T18:17:33.501Z'),
- value: '1.49609375',
- },
- graphHeight: 300,
- graphHeightOffset: 120,
- });
+ const component = createComponent(defaultValuesComponent);
expect(getCoordinate(component, '.selected-metric-line', 'x1'))
.toEqual(component.currentXCoordinate);
@@ -35,17 +38,7 @@ describe('GraphFlag', () => {
});
it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => {
- const component = createComponent({
- currentXCoordinate: 200,
- currentYCoordinate: 100,
- currentFlagPosition: 100,
- currentData: {
- time: new Date('2017-06-04T18:17:33.501Z'),
- value: '1.49609375',
- },
- graphHeight: 300,
- graphHeightOffset: 120,
- });
+ const component = createComponent(defaultValuesComponent);
const svg = component.$el.querySelector('.rect-text-metric');
expect(svg.tagName).toEqual('svg');
@@ -54,17 +47,7 @@ describe('GraphFlag', () => {
describe('Computed props', () => {
it('calculatedHeight', () => {
- const component = createComponent({
- currentXCoordinate: 200,
- currentYCoordinate: 100,
- currentFlagPosition: 100,
- currentData: {
- time: new Date('2017-06-04T18:17:33.501Z'),
- value: '1.49609375',
- },
- graphHeight: 300,
- graphHeightOffset: 120,
- });
+ const component = createComponent(defaultValuesComponent);
expect(component.calculatedHeight).toEqual(180);
});
diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js
index a4844636d09..81825a3ae87 100644
--- a/spec/javascripts/monitoring/graph_path_spec.js
+++ b/spec/javascripts/monitoring/graph_path_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import GraphPath from '~/monitoring/components/graph_path.vue';
+import GraphPath from '~/monitoring/components/graph/path.vue';
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data';
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index 7d8b0744af1..fd79abe241a 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -44,7 +44,7 @@ describe('Graph', () => {
.not.toEqual(-1);
});
- it('outterViewBox gets a width and height property based on the DOM size of the element', () => {
+ it('outerViewBox gets a width and height property based on the DOM size of the element', () => {
const component = createComponent({
graphData: convertedMetrics[1],
classType: 'col-md-6',
@@ -52,8 +52,8 @@ describe('Graph', () => {
deploymentData,
});
- const viewBoxArray = component.outterViewBox.split(' ');
- expect(typeof component.outterViewBox).toEqual('string');
+ const viewBoxArray = component.outerViewBox.split(' ');
+ expect(typeof component.outerViewBox).toEqual('string');
expect(viewBoxArray[2]).toEqual(component.graphWidth.toString());
expect(viewBoxArray[3]).toEqual(component.graphHeight.toString());
});
@@ -86,4 +86,22 @@ describe('Graph', () => {
expect(component.yAxisLabel).toEqual(component.graphData.y_label);
expect(component.legendTitle).toEqual(component.graphData.queries[0].label);
});
+
+ it('sets the currentData object based on the hovered data index', () => {
+ const component = createComponent({
+ graphData: convertedMetrics[1],
+ classType: 'col-md-6',
+ updateAspectRatio: false,
+ deploymentData,
+ graphIdentifier: 0,
+ hoverData: {
+ hoveredDate: new Date('Sun Aug 27 2017 06:11:51 GMT-0500 (CDT)'),
+ currentDeployXPos: null,
+ },
+ });
+
+ component.positionFlag();
+ expect(component.currentData).toBe(component.timeSeries[0].values[10]);
+ expect(component.currentDataIndex).toEqual(10);
+ });
});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 3e791a31604..65d2e8fd9fb 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -815,7 +815,7 @@ import '~/notes';
});
it('shows a flash message', () => {
- this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline);
+ this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
expect($('.flash-alert').is(':visible')).toBeTruthy();
});
@@ -828,7 +828,7 @@ import '~/notes';
});
it('hides visible flash message', () => {
- this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline);
+ this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
this.notes.clearFlash();
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index 256fdbe743c..4a4f2259d23 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -125,4 +125,23 @@ describe('Pipeline Url Component', () => {
component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim(),
).toEqual('Auto DevOps');
});
+
+ it('should render error badge when pipeline has a failure reason set', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {
+ failure_reason: true,
+ },
+ failure_reason: 'some reason',
+ },
+ autoDevopsHelpPath: 'foo',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-pipeline-url-failure').textContent).toContain('error');
+ expect(component.$el.querySelector('.js-pipeline-url-failure').getAttribute('data-original-title')).toContain('some reason');
+ });
});
diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js
new file mode 100644
index 00000000000..2e94948cfb2
--- /dev/null
+++ b/spec/javascripts/profile/account/components/delete_account_modal_spec.js
@@ -0,0 +1,129 @@
+import Vue from 'vue';
+
+import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue';
+
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('DeleteAccountModal component', () => {
+ const actionUrl = `${gl.TEST_HOST}/delete/user`;
+ const username = 'hasnoname';
+ let Component;
+ let vm;
+
+ beforeEach(() => {
+ Component = Vue.extend(deleteAccountModal);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ const findElements = () => {
+ const confirmation = vm.confirmWithPassword ? 'password' : 'username';
+ return {
+ form: vm.$refs.form,
+ input: vm.$el.querySelector(`[name="${confirmation}"]`),
+ submitButton: vm.$el.querySelector('.btn-danger'),
+ };
+ };
+
+ describe('with password confirmation', () => {
+ beforeEach((done) => {
+ vm = mountComponent(Component, {
+ actionUrl,
+ confirmWithPassword: true,
+ username,
+ });
+
+ vm.isOpen = true;
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not accept empty password', (done) => {
+ const { form, input, submitButton } = findElements();
+ spyOn(form, 'submit');
+ input.value = '';
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.enteredPassword).toBe(input.value);
+ expect(submitButton).toHaveClass('disabled');
+ submitButton.click();
+ expect(form.submit).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('submits form with password', (done) => {
+ const { form, input, submitButton } = findElements();
+ spyOn(form, 'submit');
+ input.value = 'anything';
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.enteredPassword).toBe(input.value);
+ expect(submitButton).not.toHaveClass('disabled');
+ submitButton.click();
+ expect(form.submit).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('with username confirmation', () => {
+ beforeEach((done) => {
+ vm = mountComponent(Component, {
+ actionUrl,
+ confirmWithPassword: false,
+ username,
+ });
+
+ vm.isOpen = true;
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not accept wrong username', (done) => {
+ const { form, input, submitButton } = findElements();
+ spyOn(form, 'submit');
+ input.value = 'this is wrong';
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.enteredUsername).toBe(input.value);
+ expect(submitButton).toHaveClass('disabled');
+ submitButton.click();
+ expect(form.submit).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('submits form with correct username', (done) => {
+ const { form, input, submitButton } = findElements();
+ spyOn(form, 'submit');
+ input.value = username;
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.enteredUsername).toBe(input.value);
+ expect(submitButton).not.toHaveClass('disabled');
+ submitButton.click();
+ expect(form.submit).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js
index f15633bd8b9..620b604f404 100644
--- a/spec/javascripts/repo/components/repo_file_spec.js
+++ b/spec/javascripts/repo/components/repo_file_spec.js
@@ -29,15 +29,17 @@ describe('RepoFile', () => {
}).$mount();
}
- beforeEach(() => {
- spyOn(repoFile.mixins[0].methods, 'timeFormated').and.returnValue(updated);
- });
-
it('renders link, icon, name and last commit details', () => {
- const vm = createComponent({
- file,
- activeFile,
+ const RepoFile = Vue.extend(repoFile);
+ const vm = new RepoFile({
+ propsData: {
+ file,
+ activeFile,
+ },
});
+ spyOn(vm, 'timeFormated').and.returnValue(updated);
+ vm.$mount();
+
const name = vm.$el.querySelector('.repo-file-name');
const fileIcon = vm.$el.querySelector('.file-icon');
diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js
index db9911c7a2c..35d2b37ac2a 100644
--- a/spec/javascripts/repo/components/repo_sidebar_spec.js
+++ b/spec/javascripts/repo/components/repo_sidebar_spec.js
@@ -5,26 +5,34 @@ import RepoStore from '~/repo/stores/repo_store';
import repoSidebar from '~/repo/components/repo_sidebar.vue';
describe('RepoSidebar', () => {
+ let vm;
+
function createComponent() {
const RepoSidebar = Vue.extend(repoSidebar);
return new RepoSidebar().$mount();
}
+ afterEach(() => {
+ vm.$destroy();
+ });
+
it('renders a sidebar', () => {
RepoStore.files = [{
id: 0,
}];
RepoStore.openedFiles = [];
- const vm = createComponent();
+ RepoStore.isRoot = false;
+
+ vm = createComponent();
const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody');
expect(vm.$el.id).toEqual('sidebar');
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
expect(thead.querySelector('.name').textContent).toEqual('Name');
- expect(thead.querySelector('.last-commit').textContent).toEqual('Last Commit');
- expect(thead.querySelector('.last-update').textContent).toEqual('Last Update');
+ expect(thead.querySelector('.last-commit').textContent).toEqual('Last commit');
+ expect(thead.querySelector('.last-update').textContent).toEqual('Last update');
expect(tbody.querySelector('.repo-file-options')).toBeFalsy();
expect(tbody.querySelector('.prev-directory')).toBeFalsy();
expect(tbody.querySelector('.loading-file')).toBeFalsy();
@@ -35,7 +43,7 @@ describe('RepoSidebar', () => {
RepoStore.openedFiles = [{
id: 0,
}];
- const vm = createComponent();
+ vm = createComponent();
expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy();
expect(vm.$el.querySelector('thead')).toBeFalsy();
@@ -47,7 +55,7 @@ describe('RepoSidebar', () => {
tree: true,
};
RepoStore.files = [];
- const vm = createComponent();
+ vm = createComponent();
expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
});
@@ -57,7 +65,7 @@ describe('RepoSidebar', () => {
id: 0,
}];
RepoStore.isRoot = true;
- const vm = createComponent();
+ vm = createComponent();
expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
});
@@ -72,7 +80,7 @@ describe('RepoSidebar', () => {
};
RepoStore.files = [file1];
RepoStore.isRoot = true;
- const vm = createComponent();
+ vm = createComponent();
vm.fileClicked(file1);
@@ -87,7 +95,7 @@ describe('RepoSidebar', () => {
spyOn(Helper, 'getFileFromPath').and.returnValue(file);
spyOn(RepoStore, 'setActiveFiles');
- const vm = createComponent();
+ vm = createComponent();
vm.fileClicked(file);
expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(file);
@@ -103,7 +111,7 @@ describe('RepoSidebar', () => {
};
RepoStore.files = [file1];
RepoStore.isRoot = true;
- const vm = createComponent();
+ vm = createComponent();
vm.fileClicked(file1);
@@ -114,12 +122,48 @@ describe('RepoSidebar', () => {
describe('goToPreviousDirectoryClicked', () => {
it('should hide files in directory if already open', () => {
const prevUrl = 'foo/bar';
- const vm = createComponent();
+ vm = createComponent();
vm.goToPreviousDirectoryClicked(prevUrl);
expect(RepoService.url).toEqual(prevUrl);
});
});
+
+ describe('back button', () => {
+ const file1 = {
+ id: 1,
+ url: 'file1',
+ };
+ const file2 = {
+ id: 2,
+ url: 'file2',
+ };
+ RepoStore.files = [file1, file2];
+ RepoStore.openedFiles = [file1, file2];
+ RepoStore.isRoot = true;
+
+ vm = createComponent();
+ vm.fileClicked(file1);
+
+ it('render previous file when using back button', () => {
+ spyOn(Helper, 'getContent').and.callThrough();
+
+ vm.fileClicked(file2);
+ expect(Helper.getContent).toHaveBeenCalledWith(file2);
+ Helper.getContent.calls.reset();
+
+ history.pushState({
+ key: Math.random(),
+ }, '', file1.url);
+ const popEvent = document.createEvent('Event');
+ popEvent.initEvent('popstate', true, true);
+ window.dispatchEvent(popEvent);
+
+ expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(file1.url);
+
+ window.history.pushState({}, null, '/');
+ });
+ });
});
});
diff --git a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
new file mode 100644
index 00000000000..b0ea8ae0206
--- /dev/null
+++ b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('EditFormButtons', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(editFormButtons);
+ const toggleForm = () => { };
+ const updateLockedAttribute = () => { };
+
+ vm1 = mountComponent(Component, {
+ isLocked: true,
+ toggleForm,
+ updateLockedAttribute,
+ });
+
+ vm2 = mountComponent(Component, {
+ isLocked: false,
+ toggleForm,
+ updateLockedAttribute,
+ });
+ });
+
+ it('renders unlock or lock text based on locked state', () => {
+ expect(
+ vm1.$el.innerHTML.includes('Unlock'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('Lock'),
+ ).toBe(true);
+ });
+});
diff --git a/spec/javascripts/sidebar/lock/edit_form_spec.js b/spec/javascripts/sidebar/lock/edit_form_spec.js
new file mode 100644
index 00000000000..7abd6997a18
--- /dev/null
+++ b/spec/javascripts/sidebar/lock/edit_form_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import editForm from '~/sidebar/components/lock/edit_form.vue';
+
+describe('EditForm', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(editForm);
+ const toggleForm = () => { };
+ const updateLockedAttribute = () => { };
+
+ vm1 = new Component({
+ propsData: {
+ isLocked: true,
+ toggleForm,
+ updateLockedAttribute,
+ issuableType: 'issue',
+ },
+ }).$mount();
+
+ vm2 = new Component({
+ propsData: {
+ isLocked: false,
+ toggleForm,
+ updateLockedAttribute,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+ });
+
+ it('renders on the appropriate warning text', () => {
+ expect(
+ vm1.$el.innerHTML.includes('Unlock this issue?'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('Lock this merge request?'),
+ ).toBe(true);
+ });
+});
diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
new file mode 100644
index 00000000000..696fca516bc
--- /dev/null
+++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
@@ -0,0 +1,71 @@
+import Vue from 'vue';
+import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
+
+describe('LockIssueSidebar', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(lockIssueSidebar);
+
+ const mediator = {
+ service: {
+ update: Promise.resolve(true),
+ },
+
+ store: {
+ isLockDialogOpen: false,
+ },
+ };
+
+ vm1 = new Component({
+ propsData: {
+ isLocked: true,
+ isEditable: true,
+ mediator,
+ issuableType: 'issue',
+ },
+ }).$mount();
+
+ vm2 = new Component({
+ propsData: {
+ isLocked: false,
+ isEditable: false,
+ mediator,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+ });
+
+ it('shows if locked and/or editable', () => {
+ expect(
+ vm1.$el.innerHTML.includes('Edit'),
+ ).toBe(true);
+
+ expect(
+ vm1.$el.innerHTML.includes('Locked'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('Unlocked'),
+ ).toBe(true);
+ });
+
+ it('displays the edit form when editable', (done) => {
+ expect(vm1.isLockDialogOpen).toBe(false);
+
+ vm1.$el.querySelector('.lock-edit').click();
+
+ expect(vm1.isLockDialogOpen).toBe(true);
+
+ vm1.$nextTick(() => {
+ expect(
+ vm1.$el
+ .innerHTML
+ .includes('Unlock this issue?'),
+ ).toBe(true);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index a160c86308d..29b15f3a782 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,72 +1,63 @@
-/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, max-len */
-/* global MockU2FDevice */
-/* global U2FAuthenticate */
-
-import '~/u2f/authenticate';
-import '~/u2f/util';
-import '~/u2f/error';
+import U2FAuthenticate from '~/u2f/authenticate';
import 'vendor/u2f';
-import './mock_u2f_device';
+import MockU2FDevice from './mock_u2f_device';
+
+describe('U2FAuthenticate', () => {
+ preloadFixtures('u2f/authenticate.html.raw');
-(function() {
- describe('U2FAuthenticate', function() {
- preloadFixtures('u2f/authenticate.html.raw');
+ beforeEach(() => {
+ loadFixtures('u2f/authenticate.html.raw');
+ this.u2fDevice = new MockU2FDevice();
+ this.container = $('#js-authenticate-u2f');
+ this.component = new U2FAuthenticate(
+ this.container,
+ '#js-login-u2f-form',
+ {
+ sign_requests: [],
+ },
+ document.querySelector('#js-login-2fa-device'),
+ document.querySelector('.js-2fa-form'),
+ );
- beforeEach(function() {
- loadFixtures('u2f/authenticate.html.raw');
- this.u2fDevice = new MockU2FDevice;
- this.container = $("#js-authenticate-u2f");
- this.component = new window.gl.U2FAuthenticate(
- this.container,
- '#js-login-u2f-form',
- {
- sign_requests: []
- },
- document.querySelector('#js-login-2fa-device'),
- document.querySelector('.js-2fa-form')
- );
+ // bypass automatic form submission within renderAuthenticated
+ spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
- // bypass automatic form submission within renderAuthenticated
- spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
+ return this.component.start();
+ });
- return this.component.start();
+ it('allows authenticating via a U2F device', () => {
+ const inProgressMessage = this.container.find('p');
+ expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
+ this.u2fDevice.respondToAuthenticateRequest({
+ deviceData: 'this is data from the device',
});
- it('allows authenticating via a U2F device', function() {
- var inProgressMessage;
- inProgressMessage = this.container.find("p");
- expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
+ expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
+ });
+
+ return describe('errors', () => {
+ it('displays an error message', () => {
+ const setupButton = this.container.find('#js-login-u2f-device');
+ setupButton.trigger('click');
this.u2fDevice.respondToAuthenticateRequest({
- deviceData: "this is data from the device"
+ errorCode: 'error!',
});
- expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
+ const errorMessage = this.container.find('p');
+ return expect(errorMessage.text()).toContain('There was a problem communicating with your device');
});
- return describe("errors", function() {
- it("displays an error message", function() {
- var errorMessage, setupButton;
- setupButton = this.container.find("#js-login-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToAuthenticateRequest({
- errorCode: "error!"
- });
- errorMessage = this.container.find("p");
- return expect(errorMessage.text()).toContain("There was a problem communicating with your device");
+ return it('allows retrying authentication after an error', () => {
+ let setupButton = this.container.find('#js-login-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ errorCode: 'error!',
});
- return it("allows retrying authentication after an error", function() {
- var retryButton, setupButton;
- setupButton = this.container.find("#js-login-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToAuthenticateRequest({
- errorCode: "error!"
- });
- retryButton = this.container.find("#js-u2f-try-again");
- retryButton.trigger('click');
- setupButton = this.container.find("#js-login-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToAuthenticateRequest({
- deviceData: "this is data from the device"
- });
- expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
+ const retryButton = this.container.find('#js-u2f-try-again');
+ retryButton.trigger('click');
+ setupButton = this.container.find('#js-login-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ deviceData: 'this is data from the device',
});
+ expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
});
-}).call(window);
+});
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index 4eb8ad3d9e4..5a1ace2b4d6 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -1,31 +1,28 @@
-/* eslint-disable space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign, max-len */
+/* eslint-disable prefer-rest-params, wrap-iife,
+no-unused-expressions, no-return-assign, no-param-reassign*/
-(function() {
- this.MockU2FDevice = (function() {
- function MockU2FDevice() {
- this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this);
- this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this);
- window.u2f || (window.u2f = {});
- window.u2f.register = (function(_this) {
- return function(appId, registerRequests, signRequests, callback) {
- return _this.registerCallback = callback;
- };
- })(this);
- window.u2f.sign = (function(_this) {
- return function(appId, challenges, signRequests, callback) {
- return _this.authenticateCallback = callback;
- };
- })(this);
- }
+export default class MockU2FDevice {
+ constructor() {
+ this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this);
+ this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this);
+ window.u2f || (window.u2f = {});
+ window.u2f.register = (function (_this) {
+ return function (appId, registerRequests, signRequests, callback) {
+ return _this.registerCallback = callback;
+ };
+ })(this);
+ window.u2f.sign = (function (_this) {
+ return function (appId, challenges, signRequests, callback) {
+ return _this.authenticateCallback = callback;
+ };
+ })(this);
+ }
- MockU2FDevice.prototype.respondToRegisterRequest = function(params) {
- return this.registerCallback(params);
- };
+ respondToRegisterRequest(params) {
+ return this.registerCallback(params);
+ }
- MockU2FDevice.prototype.respondToAuthenticateRequest = function(params) {
- return this.authenticateCallback(params);
- };
-
- return MockU2FDevice;
- })();
-}).call(window);
+ respondToAuthenticateRequest(params) {
+ return this.authenticateCallback(params);
+ }
+}
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index a445c80f2af..b0051f11362 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,77 +1,69 @@
-/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, max-len */
-/* global MockU2FDevice */
-/* global U2FRegister */
-
-import '~/u2f/register';
-import '~/u2f/util';
-import '~/u2f/error';
+import U2FRegister from '~/u2f/register';
import 'vendor/u2f';
-import './mock_u2f_device';
+import MockU2FDevice from './mock_u2f_device';
+
+describe('U2FRegister', () => {
+ preloadFixtures('u2f/register.html.raw');
-(function() {
- describe('U2FRegister', function() {
- preloadFixtures('u2f/register.html.raw');
+ beforeEach(() => {
+ loadFixtures('u2f/register.html.raw');
+ this.u2fDevice = new MockU2FDevice();
+ this.container = $('#js-register-u2f');
+ this.component = new U2FRegister(this.container, $('#js-register-u2f-templates'), {}, 'token');
+ return this.component.start();
+ });
- beforeEach(function() {
- loadFixtures('u2f/register.html.raw');
- this.u2fDevice = new MockU2FDevice;
- this.container = $("#js-register-u2f");
- this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token");
- return this.component.start();
+ it('allows registering a U2F device', () => {
+ const setupButton = this.container.find('#js-setup-u2f-device');
+ expect(setupButton.text()).toBe('Setup new U2F device');
+ setupButton.trigger('click');
+ const inProgressMessage = this.container.children('p');
+ expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
+ this.u2fDevice.respondToRegisterRequest({
+ deviceData: 'this is data from the device',
});
- it('allows registering a U2F device', function() {
- var deviceResponse, inProgressMessage, registeredMessage, setupButton;
- setupButton = this.container.find("#js-setup-u2f-device");
- expect(setupButton.text()).toBe('Setup new U2F device');
+ const registeredMessage = this.container.find('p');
+ const deviceResponse = this.container.find('#js-device-response');
+ expect(registeredMessage.text()).toContain('Your device was successfully set up!');
+ return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
+ });
+
+ return describe('errors', () => {
+ it('doesn\'t allow the same device to be registered twice (for the same user', () => {
+ const setupButton = this.container.find('#js-setup-u2f-device');
setupButton.trigger('click');
- inProgressMessage = this.container.children("p");
- expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
this.u2fDevice.respondToRegisterRequest({
- deviceData: "this is data from the device"
+ errorCode: 4,
});
- registeredMessage = this.container.find('p');
- deviceResponse = this.container.find('#js-device-response');
- expect(registeredMessage.text()).toContain("Your device was successfully set up!");
- return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
+ const errorMessage = this.container.find('p');
+ return expect(errorMessage.text()).toContain('already been registered with us');
});
- return describe("errors", function() {
- it("doesn't allow the same device to be registered twice (for the same user", function() {
- var errorMessage, setupButton;
- setupButton = this.container.find("#js-setup-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToRegisterRequest({
- errorCode: 4
- });
- errorMessage = this.container.find("p");
- return expect(errorMessage.text()).toContain("already been registered with us");
+
+ it('displays an error message for other errors', () => {
+ const setupButton = this.container.find('#js-setup-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ errorCode: 'error!',
});
- it("displays an error message for other errors", function() {
- var errorMessage, setupButton;
- setupButton = this.container.find("#js-setup-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToRegisterRequest({
- errorCode: "error!"
- });
- errorMessage = this.container.find("p");
- return expect(errorMessage.text()).toContain("There was a problem communicating with your device");
+ const errorMessage = this.container.find('p');
+ return expect(errorMessage.text()).toContain('There was a problem communicating with your device');
+ });
+
+ return it('allows retrying registration after an error', () => {
+ let setupButton = this.container.find('#js-setup-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ errorCode: 'error!',
});
- return it("allows retrying registration after an error", function() {
- var registeredMessage, retryButton, setupButton;
- setupButton = this.container.find("#js-setup-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToRegisterRequest({
- errorCode: "error!"
- });
- retryButton = this.container.find("#U2FTryAgain");
- retryButton.trigger('click');
- setupButton = this.container.find("#js-setup-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToRegisterRequest({
- deviceData: "this is data from the device"
- });
- registeredMessage = this.container.find("p");
- return expect(registeredMessage.text()).toContain("Your device was successfully set up!");
+ const retryButton = this.container.find('#U2FTryAgain');
+ retryButton.trigger('click');
+ setupButton = this.container.find('#js-setup-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ deviceData: 'this is data from the device',
});
+ const registeredMessage = this.container.find('p');
+ return expect(registeredMessage.text()).toContain('Your device was successfully set up!');
});
});
-}).call(window);
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 2422e844e97..d7019ea408b 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -12,6 +12,7 @@ const createComponent = (customConfig = {}) => {
pipeline: null,
isPipelineFailed: false,
isPipelinePassing: false,
+ isMergeAllowed: true,
onlyAllowMergeIfPipelineSucceeds: false,
hasCI: false,
ciStatus: null,
@@ -95,35 +96,84 @@ describe('MRWidgetReadyToMerge', () => {
});
});
+ describe('status', () => {
+ it('defaults to success', () => {
+ vm.mr.pipeline = true;
+ expect(vm.status).toEqual('success');
+ });
+
+ it('returns failed when MR has CI but also has an unknown status', () => {
+ vm.mr.hasCI = true;
+ expect(vm.status).toEqual('failed');
+ });
+
+ it('returns default when MR has no pipeline', () => {
+ expect(vm.status).toEqual('success');
+ });
+
+ it('returns pending when pipeline is active', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineActive = true;
+ expect(vm.status).toEqual('pending');
+ });
+
+ it('returns failed when pipeline is failed', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineFailed = true;
+ expect(vm.status).toEqual('failed');
+ });
+ });
+
describe('mergeButtonClass', () => {
const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
- it('should return default class', () => {
+ it('defaults to success class', () => {
+ expect(vm.mergeButtonClass).toEqual(defaultClass);
+ });
+
+ it('returns success class for success status', () => {
vm.mr.pipeline = true;
expect(vm.mergeButtonClass).toEqual(defaultClass);
});
- it('should return failed class when MR has CI but also has an unknown status', () => {
+ it('returns info class for pending status', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineActive = true;
+ expect(vm.mergeButtonClass).toEqual(inActionClass);
+ });
+
+ it('returns failed class for failed status', () => {
vm.mr.hasCI = true;
expect(vm.mergeButtonClass).toEqual(failedClass);
});
+ });
- it('should return default class when MR has no pipeline', () => {
- expect(vm.mergeButtonClass).toEqual(defaultClass);
+ describe('status icon', () => {
+ it('defaults to tick icon', () => {
+ expect(vm.iconClass).toEqual('success');
});
- it('should return in action class when pipeline is active', () => {
+ it('shows tick for success status', () => {
+ vm.mr.pipeline = true;
+ expect(vm.iconClass).toEqual('success');
+ });
+
+ it('shows tick for pending status', () => {
vm.mr.pipeline = {};
vm.mr.isPipelineActive = true;
- expect(vm.mergeButtonClass).toEqual(inActionClass);
+ expect(vm.iconClass).toEqual('success');
});
- it('should return failed class when pipeline is failed', () => {
- vm.mr.pipeline = {};
- vm.mr.isPipelineFailed = true;
- expect(vm.mergeButtonClass).toEqual(failedClass);
+ it('shows x for failed status', () => {
+ vm.mr.hasCI = true;
+ expect(vm.iconClass).toEqual('failed');
+ });
+
+ it('shows x for merge not allowed', () => {
+ vm.mr.hasCI = true;
+ expect(vm.iconClass).toEqual('failed');
});
});
@@ -163,21 +213,24 @@ describe('MRWidgetReadyToMerge', () => {
describe('isMergeButtonDisabled', () => {
it('should return false with initial data', () => {
+ vm.mr.isMergeAllowed = true;
expect(vm.isMergeButtonDisabled).toBeFalsy();
});
it('should return true when there is no commit message', () => {
+ vm.mr.isMergeAllowed = true;
vm.commitMessage = '';
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
it('should return true if merge is not allowed', () => {
+ vm.mr.isMergeAllowed = false;
vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
- vm.mr.isPipelineFailed = true;
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
- it('should return true when there vm instance is making request', () => {
+ it('should return true when the vm instance is making request', () => {
+ vm.mr.isMergeAllowed = true;
vm.isMakingRequest = true;
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
@@ -185,53 +238,27 @@ describe('MRWidgetReadyToMerge', () => {
});
describe('methods', () => {
- describe('isMergeAllowed', () => {
- it('should return true when no pipeline and not required to succeed', () => {
- vm.mr.onlyAllowMergeIfPipelineSucceeds = false;
- vm.mr.isPipelinePassing = false;
- expect(vm.isMergeAllowed()).toBeTruthy();
- });
-
- it('should return true when pipeline failed and not required to succeed', () => {
- vm.mr.onlyAllowMergeIfPipelineSucceeds = false;
- vm.mr.isPipelinePassing = false;
- expect(vm.isMergeAllowed()).toBeTruthy();
- });
-
- it('should return false when pipeline failed and required to succeed', () => {
- vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
- vm.mr.isPipelinePassing = false;
- expect(vm.isMergeAllowed()).toBeFalsy();
- });
-
- it('should return true when pipeline succeeded and required to succeed', () => {
- vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
- vm.mr.isPipelinePassing = true;
- expect(vm.isMergeAllowed()).toBeTruthy();
- });
- });
-
describe('shouldShowMergeControls', () => {
it('should return false when an external pipeline is running and required to succeed', () => {
- spyOn(vm, 'isMergeAllowed').and.returnValue(false);
+ vm.mr.isMergeAllowed = false;
vm.mr.isPipelineActive = false;
expect(vm.shouldShowMergeControls()).toBeFalsy();
});
it('should return true when the build succeeded or build not required to succeed', () => {
- spyOn(vm, 'isMergeAllowed').and.returnValue(true);
+ vm.mr.isMergeAllowed = true;
vm.mr.isPipelineActive = false;
expect(vm.shouldShowMergeControls()).toBeTruthy();
});
it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => {
- spyOn(vm, 'isMergeAllowed').and.returnValue(false);
+ vm.mr.isMergeAllowed = false;
vm.mr.isPipelineActive = true;
expect(vm.shouldShowMergeControls()).toBeTruthy();
});
it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => {
- spyOn(vm, 'isMergeAllowed').and.returnValue(true);
+ vm.mr.isMergeAllowed = true;
vm.mr.isPipelineActive = true;
expect(vm.shouldShowMergeControls()).toBeTruthy();
});
diff --git a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
index b63633c03b8..e667b4b3677 100644
--- a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
+++ b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
@@ -31,6 +31,7 @@ describe('MRWidgetService', () => {
});
it('should have methods defined', () => {
+ window.history.pushState({}, null, '/');
const service = new MRWidgetService(mr);
expect(service.merge()).toBeDefined();
diff --git a/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js
deleted file mode 100644
index 6df08f3ebe7..00000000000
--- a/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import Vue from 'vue';
-import confidentialIssue from '~/vue_shared/components/issue/confidential_issue_warning.vue';
-
-describe('Confidential Issue Warning Component', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(confidentialIssue);
- vm = new Component().$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render confidential issue warning information', () => {
- expect(vm.$el.querySelector('i').className).toEqual('fa fa-eye-slash');
- expect(vm.$el.querySelector('span').textContent.trim()).toEqual('This is a confidential issue. Your comment will not be visible to the public.');
- });
-});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
new file mode 100644
index 00000000000..2cf4d8e00ed
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
@@ -0,0 +1,46 @@
+import Vue from 'vue';
+import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+const IssueWarning = Vue.extend(issueWarning);
+
+function formatWarning(string) {
+ // Replace newlines with a space then replace multiple spaces with one space
+ return string.trim().replace(/\n/g, ' ').replace(/\s\s+/g, ' ');
+}
+
+describe('Issue Warning Component', () => {
+ describe('isLocked', () => {
+ it('should render locked issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isLocked: true,
+ });
+
+ expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-lock');
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is locked. Only project members can comment.');
+ });
+ });
+
+ describe('isConfidential', () => {
+ it('should render confidential issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isConfidential: true,
+ });
+
+ expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-eye-slash');
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This is a confidential issue. Your comment will not be visible to the public.');
+ });
+ });
+
+ describe('isLocked and isConfidential', () => {
+ it('should render locked and confidential issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isLocked: true,
+ isConfidential: true,
+ });
+
+ expect(vm.$el.querySelector('i')).toBeFalsy();
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is confidential and locked. People without permission will never get a notification and won\'t be able to comment.');
+ });
+ });
+});
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index da42272bbef..81a04a2d46d 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -31,7 +31,14 @@ describe Banzai::Renderer do
let(:object) { fake_object(fresh: false) }
it 'caches and returns the result' do
- expect(object).to receive(:refresh_markdown_cache!).with(do_update: true)
+ expect(object).to receive(:refresh_markdown_cache!)
+
+ is_expected.to eq('field_html')
+ end
+
+ it "skips database caching on a GitLab read-only instance" do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ expect(object).to receive(:refresh_markdown_cache!)
is_expected.to eq('field_html')
end
diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
new file mode 100644
index 00000000000..1a4ea2bac48
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migration, schema: 20170929131201 do
+ let(:migration) { described_class.new }
+
+ let(:base1) { create(:project) }
+ let(:base1_fork1) { create(:project) }
+ let(:base1_fork2) { create(:project) }
+
+ let(:base2) { create(:project) }
+ let(:base2_fork1) { create(:project) }
+ let(:base2_fork2) { create(:project) }
+
+ let(:fork_of_fork) { create(:project) }
+ let(:fork_of_fork2) { create(:project) }
+ let(:second_level_fork) { create(:project) }
+ let(:third_level_fork) { create(:project) }
+
+ let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) }
+ let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) }
+
+ let!(:forked_project_links) { table(:forked_project_links) }
+ let!(:fork_networks) { table(:fork_networks) }
+ let!(:fork_network_members) { table(:fork_network_members) }
+
+ before do
+ # The fork-network relation created for the forked project
+ fork_networks.create(id: 1, root_project_id: base1.id)
+ fork_network_members.create(project_id: base1.id, fork_network_id: 1)
+ fork_networks.create(id: 2, root_project_id: base2.id)
+ fork_network_members.create(project_id: base2.id, fork_network_id: 2)
+
+ # Normal fork links
+ forked_project_links.create(id: 1, forked_from_project_id: base1.id, forked_to_project_id: base1_fork1.id)
+ forked_project_links.create(id: 2, forked_from_project_id: base1.id, forked_to_project_id: base1_fork2.id)
+ forked_project_links.create(id: 3, forked_from_project_id: base2.id, forked_to_project_id: base2_fork1.id)
+ forked_project_links.create(id: 4, forked_from_project_id: base2.id, forked_to_project_id: base2_fork2.id)
+
+ # Fork links
+ forked_project_links.create(id: 5, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork.id)
+ forked_project_links.create(id: 6, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork2.id)
+
+ # Forks 3 levels down
+ forked_project_links.create(id: 7, forked_from_project_id: fork_of_fork.id, forked_to_project_id: second_level_fork.id)
+ forked_project_links.create(id: 8, forked_from_project_id: second_level_fork.id, forked_to_project_id: third_level_fork.id)
+
+ migration.perform(1, 8)
+ end
+
+ it 'creates a memberships for the direct forks' do
+ base1_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id,
+ project_id: base1_fork1.id)
+ base1_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network1.id,
+ project_id: base1_fork2.id)
+ base2_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network2.id,
+ project_id: base2_fork1.id)
+ base2_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id,
+ project_id: base2_fork2.id)
+
+ expect(base1_fork1_membership.forked_from_project_id).to eq(base1.id)
+ expect(base1_fork2_membership.forked_from_project_id).to eq(base1.id)
+ expect(base2_fork1_membership.forked_from_project_id).to eq(base2.id)
+ expect(base2_fork2_membership.forked_from_project_id).to eq(base2.id)
+ end
+
+ it 'adds the fork network members for forks of forks' do
+ fork_of_fork_membership = fork_network_members.find_by(project_id: fork_of_fork.id,
+ fork_network_id: fork_network1.id)
+ fork_of_fork2_membership = fork_network_members.find_by(project_id: fork_of_fork2.id,
+ fork_network_id: fork_network1.id)
+ second_level_fork_membership = fork_network_members.find_by(project_id: second_level_fork.id,
+ fork_network_id: fork_network1.id)
+ third_level_fork_membership = fork_network_members.find_by(project_id: third_level_fork.id,
+ fork_network_id: fork_network1.id)
+
+ expect(fork_of_fork_membership.forked_from_project_id).to eq(base1_fork1.id)
+ expect(fork_of_fork2_membership.forked_from_project_id).to eq(base1_fork1.id)
+ expect(second_level_fork_membership.forked_from_project_id).to eq(fork_of_fork.id)
+ expect(third_level_fork_membership.forked_from_project_id).to eq(second_level_fork.id)
+ end
+
+ it 'reschedules itself when there are missing members' do
+ allow(migration).to receive(:missing_members?).and_return(true)
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in).with(described_class::RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [1, 3])
+
+ migration.perform(1, 3)
+ end
+
+ it 'can be repeated without effect' do
+ expect { fork_network_members.count }.not_to change { migration.perform(1, 7) }
+ end
+
+ it 'knows it is finished for this range' do
+ expect(migration.missing_members?(1, 7)).to be_falsy
+ end
+
+ context 'with more forks' do
+ before do
+ forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id)
+ forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id)
+ end
+
+ it 'only processes a single batch of links at a time' do
+ expect(fork_network_members.count).to eq(10)
+
+ migration.perform(8, 10)
+
+ expect(fork_network_members.count).to eq(12)
+ end
+
+ it 'knows when not all memberships withing a batch have been created' do
+ expect(migration.missing_members?(8, 10)).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb
new file mode 100644
index 00000000000..26d48cc8201
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys, :migration, schema: 20171005130944 do
+ context 'when GpgKey exists' do
+ let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) }
+
+ before do
+ GpgKeySubkey.destroy_all
+ end
+
+ it 'generate the subkeys' do
+ expect do
+ described_class.new.perform(gpg_key.id)
+ end.to change { gpg_key.subkeys.count }.from(0).to(2)
+ end
+
+ it 'schedules the signature update worker' do
+ expect(InvalidGpgSignatureUpdateWorker).to receive(:perform_async).with(gpg_key.id)
+
+ described_class.new.perform(gpg_key.id)
+ end
+ end
+
+ context 'when GpgKey does not exist' do
+ it 'does not do anything' do
+ expect(Gitlab::Gpg).not_to receive(:subkeys_from_key)
+ expect(InvalidGpgSignatureUpdateWorker).not_to receive(:perform_async)
+
+ described_class.new.perform(123)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
index 59f69d1e4b1..7b5a00c6111 100644
--- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do
end
describe '#perform' do
- it 'renames the path of system-uploads', truncate: true do
+ it 'renames the path of system-uploads', :truncate do
upload = create(:upload, model: create(:project), path: 'uploads/system/project/avatar.jpg')
migration.perform('uploads/system/', 'uploads/-/system/')
diff --git a/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb b/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb
new file mode 100644
index 00000000000..dfbf1bb681a
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::NormalizeLdapExternUidsRange, :migration, schema: 20170921101004 do
+ let!(:identities) { table(:identities) }
+
+ before do
+ # LDAP identities
+ (1..4).each do |i|
+ identities.create!(id: i, provider: 'ldapmain', extern_uid: " uid = foo #{i}, ou = People, dc = example, dc = com ", user_id: i)
+ end
+
+ # Non-LDAP identity
+ identities.create!(id: 5, provider: 'foo', extern_uid: " uid = foo 5, ou = People, dc = example, dc = com ", user_id: 5)
+
+ # Another LDAP identity
+ identities.create!(id: 6, provider: 'ldapmain', extern_uid: " uid = foo 6, ou = People, dc = example, dc = com ", user_id: 6)
+ end
+
+ it 'normalizes the LDAP identities in the range' do
+ described_class.new.perform(1, 3)
+ expect(identities.find(1).extern_uid).to eq("uid=foo 1,ou=people,dc=example,dc=com")
+ expect(identities.find(2).extern_uid).to eq("uid=foo 2,ou=people,dc=example,dc=com")
+ expect(identities.find(3).extern_uid).to eq("uid=foo 3,ou=people,dc=example,dc=com")
+ expect(identities.find(4).extern_uid).to eq(" uid = foo 4, ou = People, dc = example, dc = com ")
+ expect(identities.find(5).extern_uid).to eq(" uid = foo 5, ou = People, dc = example, dc = com ")
+ expect(identities.find(6).extern_uid).to eq(" uid = foo 6, ou = People, dc = example, dc = com ")
+
+ described_class.new.perform(4, 6)
+ expect(identities.find(1).extern_uid).to eq("uid=foo 1,ou=people,dc=example,dc=com")
+ expect(identities.find(2).extern_uid).to eq("uid=foo 2,ou=people,dc=example,dc=com")
+ expect(identities.find(3).extern_uid).to eq("uid=foo 3,ou=people,dc=example,dc=com")
+ expect(identities.find(4).extern_uid).to eq("uid=foo 4,ou=people,dc=example,dc=com")
+ expect(identities.find(5).extern_uid).to eq(" uid = foo 5, ou = People, dc = example, dc = com ")
+ expect(identities.find(6).extern_uid).to eq("uid=foo 6,ou=people,dc=example,dc=com")
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
new file mode 100644
index 00000000000..2c2684a6fc9
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do
+ let(:migration) { described_class.new }
+ let(:base1) { create(:project) }
+ let(:base1_fork1) { create(:project) }
+ let(:base1_fork2) { create(:project) }
+
+ let(:base2) { create(:project) }
+ let(:base2_fork1) { create(:project) }
+ let(:base2_fork2) { create(:project) }
+
+ let!(:forked_project_links) { table(:forked_project_links) }
+ let!(:fork_networks) { table(:fork_networks) }
+ let!(:fork_network_members) { table(:fork_network_members) }
+
+ let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) }
+ let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) }
+
+ before do
+ # A normal fork link
+ forked_project_links.create(id: 1,
+ forked_from_project_id: base1.id,
+ forked_to_project_id: base1_fork1.id)
+ forked_project_links.create(id: 2,
+ forked_from_project_id: base1.id,
+ forked_to_project_id: base1_fork2.id)
+
+ forked_project_links.create(id: 3,
+ forked_from_project_id: base2.id,
+ forked_to_project_id: base2_fork1.id)
+ forked_project_links.create(id: 4,
+ forked_from_project_id: base2_fork1.id,
+ forked_to_project_id: create(:project).id)
+
+ forked_project_links.create(id: 5,
+ forked_from_project_id: base2.id,
+ forked_to_project_id: base2_fork2.id)
+
+ migration.perform(1, 3)
+ end
+
+ it 'it creates the fork network' do
+ expect(fork_network1).not_to be_nil
+ expect(fork_network2).not_to be_nil
+ end
+
+ it 'does not create a fork network for a fork-of-fork' do
+ # perfrom the entire batch
+ migration.perform(1, 5)
+
+ expect(fork_networks.find_by(root_project_id: base2_fork1.id)).to be_nil
+ end
+
+ it 'creates memberships for the root of fork networks' do
+ base1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id,
+ project_id: base1.id)
+ base2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id,
+ project_id: base2.id)
+
+ expect(base1_membership).not_to be_nil
+ expect(base2_membership).not_to be_nil
+ end
+
+ it 'skips links that had their source project deleted' do
+ forked_project_links.create(id: 6, forked_from_project_id: 99999, forked_to_project_id: create(:project).id)
+
+ migration.perform(5, 8)
+
+ expect(fork_networks.find_by(root_project_id: 99999)).to be_nil
+ end
+
+ it 'schedules a job for inserting memberships for forks-of-forks' do
+ delay = Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in).with(delay, "CreateForkNetworkMembershipsRange", [1, 3])
+
+ migration.perform(1, 3)
+ end
+
+ it 'only processes a single batch of links at a time' do
+ expect(fork_network_members.count).to eq(5)
+
+ migration.perform(3, 5)
+
+ expect(fork_network_members.count).to eq(7)
+ end
+
+ it 'can be repeated without effect' do
+ expect { migration.perform(1, 3) }.not_to change { fork_network_members.count }
+ end
+end
diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
index 2c7ef622c51..633e319f46d 100644
--- a/spec/lib/gitlab/checks/force_push_spec.rb
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Checks::ForcePush do
let(:project) { create(:project, :repository) }
- context "exit code checking", skip_gitaly_mock: true do
+ context "exit code checking", :skip_gitaly_mock do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['normal output', 0])
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
index 3740df88f42..8357af38f92 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
@@ -55,6 +55,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
it 'fails the pipeline' do
expect(pipeline.reload).to be_failed
end
+
+ it 'sets a config error failure reason' do
+ expect(pipeline.reload.config_error?).to eq true
+ end
end
context 'when saving incomplete pipeline is not allowed' do
diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb
index 9ecd128faca..3fe8d50c49a 100644
--- a/spec/lib/gitlab/ci/stage/seed_spec.rb
+++ b/spec/lib/gitlab/ci/stage/seed_spec.rb
@@ -11,6 +11,12 @@ describe Gitlab::Ci::Stage::Seed do
described_class.new(pipeline, 'test', builds)
end
+ describe '#size' do
+ it 'returns a number of jobs in the stage' do
+ expect(subject.size).to eq 2
+ end
+ end
+
describe '#stage' do
it 'returns hash attributes of a stage' do
expect(subject.stage).to be_a Hash
diff --git a/spec/lib/gitlab/ci/trace/section_parser_spec.rb b/spec/lib/gitlab/ci/trace/section_parser_spec.rb
new file mode 100644
index 00000000000..ca53ff87c6f
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace/section_parser_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Trace::SectionParser do
+ def lines_with_pos(text)
+ pos = 0
+ StringIO.new(text).each_line do |line|
+ yield line, pos
+ pos += line.bytesize + 1 # newline
+ end
+ end
+
+ def build_lines(text)
+ to_enum(:lines_with_pos, text)
+ end
+
+ def section(name, start, duration, text)
+ end_ = start + duration
+ "section_start:#{start.to_i}:#{name}\r\033[0K#{text}section_end:#{end_.to_i}:#{name}\r\033[0K"
+ end
+
+ let(:lines) { build_lines('') }
+ subject { described_class.new(lines) }
+
+ describe '#sections' do
+ before do
+ subject.parse!
+ end
+
+ context 'empty trace' do
+ let(:lines) { build_lines('') }
+
+ it { expect(subject.sections).to be_empty }
+ end
+
+ context 'with a sectionless trace' do
+ let(:lines) { build_lines("line 1\nline 2\n") }
+
+ it { expect(subject.sections).to be_empty }
+ end
+
+ context 'with trace markers' do
+ let(:start_time) { Time.new(2017, 10, 5).utc }
+ let(:section_b_duration) { 1.second }
+ let(:section_a) { section('a', start_time, 0, 'a line') }
+ let(:section_b) { section('b', start_time, section_b_duration, "another line\n") }
+ let(:lines) { build_lines(section_a + section_b) }
+
+ it { expect(subject.sections.size).to eq(2) }
+ it { expect(subject.sections[1][:name]).to eq('b') }
+ it { expect(subject.sections[1][:date_start]).to eq(start_time) }
+ it { expect(subject.sections[1][:date_end]).to eq(start_time + section_b_duration) }
+ end
+ end
+
+ describe '#parse!' do
+ context 'multiple "section_" but no complete markers' do
+ let(:lines) { build_lines('section_section_section_') }
+
+ it 'must find 3 possible section start but no complete sections' do
+ expect(subject).to receive(:find_next_marker).exactly(3).times.and_call_original
+
+ subject.parse!
+
+ expect(subject.sections).to be_empty
+ end
+ end
+
+ context 'trace with UTF-8 chars' do
+ let(:line) { 'GitLab ❤️ 狸 (tanukis)\n' }
+ let(:trace) { section('test_section', Time.new(2017, 10, 5).utc, 3.seconds, line) }
+ let(:lines) { build_lines(trace) }
+
+ it 'must handle correctly byte positioning' do
+ expect(subject).to receive(:find_next_marker).exactly(2).times.and_call_original
+
+ subject.parse!
+
+ sections = subject.sections
+
+ expect(sections.size).to eq(1)
+ s = sections[0]
+ len = s[:byte_end] - s[:byte_start]
+ expect(trace.byteslice(s[:byte_start], len)).to eq(line)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 9cb0b62590a..3546532b9b4 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -61,6 +61,93 @@ describe Gitlab::Ci::Trace do
end
end
+ describe '#extract_sections' do
+ let(:log) { 'No sections' }
+ let(:sections) { trace.extract_sections }
+
+ before do
+ trace.set(log)
+ end
+
+ context 'no sections' do
+ it 'returs []' do
+ expect(trace.extract_sections).to eq([])
+ end
+ end
+
+ context 'multiple sections available' do
+ let(:log) { File.read(expand_fixture_path('trace/trace_with_sections')) }
+ let(:sections_data) do
+ [
+ { name: 'prepare_script', lines: 2, duration: 3.seconds },
+ { name: 'get_sources', lines: 4, duration: 1.second },
+ { name: 'restore_cache', lines: 0, duration: 0.seconds },
+ { name: 'download_artifacts', lines: 0, duration: 0.seconds },
+ { name: 'build_script', lines: 2, duration: 1.second },
+ { name: 'after_script', lines: 0, duration: 0.seconds },
+ { name: 'archive_cache', lines: 0, duration: 0.seconds },
+ { name: 'upload_artifacts', lines: 0, duration: 0.seconds }
+ ]
+ end
+
+ it "returns valid sections" do
+ expect(sections).not_to be_empty
+ expect(sections.size).to eq(sections_data.size),
+ "expected #{sections_data.size} sections, got #{sections.size}"
+
+ buff = StringIO.new(log)
+ sections.each_with_index do |s, i|
+ expected = sections_data[i]
+
+ expect(s[:name]).to eq(expected[:name])
+ expect(s[:date_end] - s[:date_start]).to eq(expected[:duration])
+
+ buff.seek(s[:byte_start], IO::SEEK_SET)
+ length = s[:byte_end] - s[:byte_start]
+ lines = buff.read(length).count("\n")
+ expect(lines).to eq(expected[:lines])
+ end
+ end
+ end
+
+ context 'logs contains "section_start"' do
+ let(:log) { "section_start:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_end:1506417477:a_section\r\033[0K"}
+
+ it "returns only one section" do
+ expect(sections).not_to be_empty
+ expect(sections.size).to eq(1)
+
+ section = sections[0]
+ expect(section[:name]).to eq('a_section')
+ expect(section[:byte_start]).not_to eq(section[:byte_end]), "got an empty section"
+ end
+ end
+
+ context 'missing section_end' do
+ let(:log) { "section_start:1506417476:a_section\r\033[0KSome logs\nNo section_end\n"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+
+ context 'missing section_start' do
+ let(:log) { "Some logs\nNo section_start\nsection_end:1506417476:a_section\r\033[0K"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+
+ context 'inverted section_start section_end' do
+ let(:log) { "section_end:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_start:1506417477:a_section\r\033[0K"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+ end
+
describe '#set' do
before do
trace.set("12")
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
index 90aa4f63dd5..596cc435bd9 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
@@ -229,7 +229,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca
end
end
- describe '#track_rename', redis: true do
+ describe '#track_rename', :redis do
it 'tracks a rename in redis' do
key = 'rename:FakeRenameReservedPathMigrationV1:namespace'
@@ -246,7 +246,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca
end
end
- describe '#reverts_for_type', redis: true do
+ describe '#reverts_for_type', :redis do
it 'yields for each tracked rename' do
subject.track_rename('project', 'old_path', 'new_path')
subject.track_rename('project', 'old_path2', 'new_path2')
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
index 32ac0b88a9b..1143182531f 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
@@ -241,7 +241,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :
end
end
- describe '#revert_renames', redis: true do
+ describe '#revert_renames', :redis do
it 'renames the routes back to the previous values' do
project = create(:project, :repository, path: 'a-project', namespace: namespace)
subject.rename_namespace(namespace)
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
index 595e06a9748..8922370b0a0 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
@@ -115,7 +115,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
end
end
- describe '#revert_renames', redis: true do
+ describe '#revert_renames', :redis do
it 'renames the routes back to the previous values' do
subject.rename_project(project)
diff --git a/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb
new file mode 100644
index 00000000000..2f99febe04e
--- /dev/null
+++ b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::Formatters::ImageFormatter do
+ it_behaves_like "position formatter" do
+ let(:base_attrs) do
+ {
+ base_sha: 123,
+ start_sha: 456,
+ head_sha: 789,
+ old_path: 'old_image.png',
+ new_path: 'new_image.png',
+ position_type: 'image'
+ }
+ end
+
+ let(:attrs) do
+ base_attrs.merge(width: 100, height: 100, x: 1, y: 2)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb
new file mode 100644
index 00000000000..897dc917f6a
--- /dev/null
+++ b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::Formatters::TextFormatter do
+ let!(:base) do
+ {
+ base_sha: 123,
+ start_sha: 456,
+ head_sha: 789,
+ old_path: 'old_path.txt',
+ new_path: 'new_path.txt'
+ }
+ end
+
+ let!(:complete) do
+ base.merge(old_line: 1, new_line: 2)
+ end
+
+ it_behaves_like "position formatter" do
+ let(:base_attrs) { base }
+
+ let(:attrs) { complete }
+ end
+
+ # Specific text formatter examples
+ let!(:formatter) { described_class.new(attrs) }
+
+ describe '#line_age' do
+ subject { formatter.line_age }
+
+ context ' when there is only new_line' do
+ let(:attrs) { base.merge(new_line: 1) }
+
+ it { is_expected.to eq('new') }
+ end
+
+ context ' when there is only old_line' do
+ let(:attrs) { base.merge(old_line: 1) }
+
+ it { is_expected.to eq('old') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index 7798736a4dc..9bf54fdecc4 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Diff::Position do
let(:project) { create(:project, :repository) }
- describe "position for an added file" do
+ describe "position for an added text file" do
let(:commit) { project.commit("2ea1f3dec713d940208fb5ce4a38765ecb5d3f73") }
subject do
@@ -47,6 +47,31 @@ describe Gitlab::Diff::Position do
end
end
+ describe "position for an added image file" do
+ let(:commit) { project.commit("33f3729a45c02fc67d00adb1b8bca394b0e761d9") }
+
+ subject do
+ described_class.new(
+ old_path: "files/images/6049019_460s.jpg",
+ new_path: "files/images/6049019_460s.jpg",
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 100,
+ diff_refs: commit.diff_refs,
+ position_type: "image"
+ )
+ end
+
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.new_file?).to be true
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
describe "position for a changed file" do
let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") }
@@ -468,26 +493,54 @@ describe Gitlab::Diff::Position do
end
describe "#to_json" do
- let(:hash) do
- {
- old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: 14,
- base_sha: nil,
- head_sha: nil,
- start_sha: nil
- }
+ shared_examples "diff position json" do
+ it "returns the position as JSON" do
+ expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys)
+ end
+
+ it "works when nested under another hash" do
+ expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys)
+ end
end
- let(:diff_position) { described_class.new(hash) }
+ context "for text positon" do
+ let(:hash) do
+ {
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ base_sha: nil,
+ head_sha: nil,
+ start_sha: nil,
+ position_type: "text"
+ }
+ end
+
+ let(:diff_position) { described_class.new(hash) }
- it "returns the position as JSON" do
- expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys)
+ it_behaves_like "diff position json"
end
- it "works when nested under another hash" do
- expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys)
+ context "for image positon" do
+ let(:hash) do
+ {
+ old_path: "files/any.img",
+ new_path: "files/any.img",
+ base_sha: nil,
+ head_sha: nil,
+ start_sha: nil,
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 100,
+ position_type: "image"
+ }
+ end
+
+ let(:diff_position) { described_class.new(hash) }
+
+ it_behaves_like "diff position json"
end
end
end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index 4fa30d8df8b..e5138705443 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -71,6 +71,10 @@ describe Gitlab::Diff::PositionTracer do
Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id)
end
+ def text_position_attrs
+ [:old_line, :new_line]
+ end
+
def position(attrs = {})
attrs.reverse_merge!(
diff_refs: old_diff_refs
@@ -91,7 +95,11 @@ describe Gitlab::Diff::PositionTracer do
expect(new_position.diff_refs).to eq(new_diff_refs)
attrs.each do |attr, value|
- expect(new_position.send(attr)).to eq(value)
+ if text_position_attrs.include?(attr)
+ expect(new_position.formatter.send(attr)).to eq(value)
+ else
+ expect(new_position.send(attr)).to eq(value)
+ end
end
end
end
@@ -110,7 +118,11 @@ describe Gitlab::Diff::PositionTracer do
expect(change_position.diff_refs).to eq(change_diff_refs)
attrs.each do |attr, value|
- expect(change_position.send(attr)).to eq(value)
+ if text_position_attrs.include?(attr)
+ expect(change_position.formatter.send(attr)).to eq(value)
+ else
+ expect(change_position.send(attr)).to eq(value)
+ end
end
end
end
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 465c2012b05..793228701cf 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -73,7 +73,7 @@ describe Gitlab::Git::Blame, seed_helper: true do
it_behaves_like 'blaming a file'
end
- context 'when Gitaly blame feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly blame feature is disabled', :skip_gitaly_mock do
it_behaves_like 'blaming a file'
end
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index f3945e748ab..412a0093d97 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -112,7 +112,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
it_behaves_like 'finding blobs'
end
- context 'when project_raw_show Gitaly feature is disabled', skip_gitaly_mock: true do
+ context 'when project_raw_show Gitaly feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding blobs'
end
end
@@ -150,7 +150,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
it_behaves_like 'finding blobs by ID'
end
- context 'when the blob_raw Gitaly feature is disabled', skip_gitaly_mock: true do
+ context 'when the blob_raw Gitaly feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding blobs by ID'
end
end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 3815055139a..9f4e3c49adc 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -261,7 +261,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it_should_behave_like '.where'
end
- describe '.where without gitaly', skip_gitaly_mock: true do
+ describe '.where without gitaly', :skip_gitaly_mock do
it_should_behave_like '.where'
end
@@ -336,7 +336,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it_behaves_like 'finding all commits'
end
- context 'when Gitaly find_all_commits feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly find_all_commits feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding all commits'
context 'while applying a sort order based on the `order` option' do
@@ -405,7 +405,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it_should_behave_like '#stats'
end
- describe '#stats with gitaly disabled', skip_gitaly_mock: true do
+ describe '#stats with gitaly disabled', :skip_gitaly_mock do
it_should_behave_like '#stats'
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 5f12125beb2..1ee4acfd193 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -54,7 +54,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#rugged" do
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'raises a storage exception when storage is not available' do
broken_repo = described_class.new('broken', 'a/path.git', '')
@@ -384,7 +384,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- context 'when Gitaly commit_count feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly commit_count feature is disabled', :skip_gitaly_mock do
it_behaves_like 'simple commit counting'
end
end
@@ -418,7 +418,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'check for local branches'
end
- context 'without gitaly', skip_gitaly_mock: true do
+ context 'without gitaly', :skip_gitaly_mock do
it_behaves_like 'check for local branches'
end
end
@@ -453,7 +453,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like "deleting a branch"
end
- context "when Gitaly delete_branch is disabled", skip_gitaly_mock: true do
+ context "when Gitaly delete_branch is disabled", :skip_gitaly_mock do
it_behaves_like "deleting a branch"
end
end
@@ -489,7 +489,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'creating a branch'
end
- context 'when Gitaly create_branch feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly create_branch feature is disabled', :skip_gitaly_mock do
it_behaves_like 'creating a branch'
end
end
@@ -929,7 +929,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'extended commit counting'
end
- context 'when Gitaly count_commits feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly count_commits feature is disabled', :skip_gitaly_mock do
it_behaves_like 'extended commit counting'
end
end
@@ -996,7 +996,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'finding a branch'
end
- context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding a branch'
it 'should reload Rugged::Repository and return master' do
@@ -1238,7 +1238,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'checks the existence of refs'
end
- context 'when Gitaly ref_exists feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly ref_exists feature is disabled', :skip_gitaly_mock do
it_behaves_like 'checks the existence of refs'
end
end
@@ -1260,7 +1260,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'checks the existence of tags'
end
- context 'when Gitaly ref_exists_tags feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly ref_exists_tags feature is disabled', :skip_gitaly_mock do
it_behaves_like 'checks the existence of tags'
end
end
@@ -1284,7 +1284,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'checks the existence of branches'
end
- context 'when Gitaly ref_exists_branches feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly ref_exists_branches feature is disabled', :skip_gitaly_mock do
it_behaves_like 'checks the existence of branches'
end
end
@@ -1361,7 +1361,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'languages'
- context 'with rugged', skip_gitaly_mock: true do
+ context 'with rugged', :skip_gitaly_mock do
it_behaves_like 'languages'
end
end
@@ -1467,7 +1467,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like "user deleting a branch"
end
- context "when Gitaly user_delete_branch is disabled", skip_gitaly_mock: true do
+ context "when Gitaly user_delete_branch is disabled", :skip_gitaly_mock do
it_behaves_like "user deleting a branch"
end
end
diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
index cc10679ef1e..6c4f538bf01 100644
--- a/spec/lib/gitlab/git/tag_spec.rb
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Git::Tag, seed_helper: true do
it_behaves_like 'Gitlab::Git::Repository#tags'
end
- context 'when Gitaly tags feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly tags feature is disabled', :skip_gitaly_mock do
it_behaves_like 'Gitlab::Git::Repository#tags'
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 458627ee4de..c9643c5da47 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -165,7 +165,7 @@ describe Gitlab::GitAccess do
stub_application_setting(rsa_key_restriction: 4096)
end
- it 'does not allow keys which are too small', aggregate_failures: true do
+ it 'does not allow keys which are too small', :aggregate_failures do
expect(actor).not_to be_valid
expect { pull_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.')
expect { push_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.')
@@ -177,7 +177,7 @@ describe Gitlab::GitAccess do
stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE)
end
- it 'does not allow keys which are too small', aggregate_failures: true do
+ it 'does not allow keys which are too small', :aggregate_failures do
expect(actor).not_to be_valid
expect { pull_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/)
expect { push_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/)
@@ -598,6 +598,19 @@ describe Gitlab::GitAccess do
admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }))
end
end
+
+ context "when in a read-only GitLab instance" do
+ before do
+ create(:protected_branch, name: 'feature', project: project)
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ # Only check admin; if an admin can't do it, other roles can't either
+ matrix = permissions_matrix[:admin].dup
+ matrix.each { |key, _| matrix[key] = false }
+
+ run_permission_checks(admin: matrix)
+ end
end
describe 'build authentication abilities' do
@@ -632,6 +645,16 @@ describe Gitlab::GitAccess do
end
end
+ context 'when the repository is read only' do
+ let(:project) { create(:project, :repository, :read_only) }
+
+ it 'denies push access' do
+ project.add_master(user)
+
+ expect { push_access_check }.to raise_unauthorized('The repository is temporarily read-only. Please try again later.')
+ end
+ end
+
describe 'deploy key permissions' do
let(:key) { create(:deploy_key, user: user, can_push: can_push) }
let(:actor) { key }
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 0376b4ee783..1056074264a 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::GitAccessWiki do
let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
+ let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] }
let(:redirected_path) { nil }
let(:authentication_abilities) do
[
@@ -13,19 +14,27 @@ describe Gitlab::GitAccessWiki do
]
end
- describe 'push_allowed?' do
- before do
- create(:protected_branch, name: 'master', project: project)
- project.team << [user, :developer]
- end
+ describe '#push_access_check' do
+ context 'when user can :create_wiki' do
+ before do
+ create(:protected_branch, name: 'master', project: project)
+ project.team << [user, :developer]
+ end
- subject { access.check('git-receive-pack', changes) }
+ subject { access.check('git-receive-pack', changes) }
- it { expect { subject }.not_to raise_error }
- end
+ it { expect { subject }.not_to raise_error }
+
+ context 'when in a read-only GitLab instance' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
- def changes
- ['6f6d7e7ed 570e7b2ab refs/heads/master']
+ it 'does not give access to upload wiki code' do
+ expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "You can't push code to a read-only GitLab instance.")
+ end
+ end
+ end
end
describe '#access_check_download!' do
diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb
index e1fa8ae03f8..ba7fb168a3b 100644
--- a/spec/lib/gitlab/git_ref_validator_spec.rb
+++ b/spec/lib/gitlab/git_ref_validator_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::GitRefValidator do
it { expect(described_class.validate('feature/new')).to be_truthy }
it { expect(described_class.validate('implement_@all')).to be_truthy }
it { expect(described_class.validate('my_new_feature')).to be_truthy }
+ it { expect(described_class.validate('my-branch')).to be_truthy }
it { expect(described_class.validate('#1')).to be_truthy }
it { expect(described_class.validate('feature/refs/heads/foo')).to be_truthy }
it { expect(described_class.validate('feature/~new/')).to be_falsey }
@@ -22,4 +23,8 @@ describe Gitlab::GitRefValidator do
it { expect(described_class.validate('refs/remotes/')).to be_falsey }
it { expect(described_class.validate('refs/heads/feature')).to be_falsey }
it { expect(described_class.validate('refs/remotes/origin')).to be_falsey }
+ it { expect(described_class.validate('-')).to be_falsey }
+ it { expect(described_class.validate('-branch')).to be_falsey }
+ it { expect(described_class.validate('.tag')).to be_falsey }
+ it { expect(described_class.validate('my branch')).to be_falsey }
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 6f59750b4da..8127b4842b7 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -84,14 +84,14 @@ describe Gitlab::GitalyClient::RefService do
end
end
- describe '#find_ref_name', seed_helper: true do
+ describe '#find_ref_name', :seed_helper do
subject { client.find_ref_name(SeedRepo::Commit::ID, 'refs/heads/master') }
it { is_expected.to be_utf8 }
it { is_expected.to eq('refs/heads/master') }
end
- describe '#ref_exists?', seed_helper: true do
+ describe '#ref_exists?', :seed_helper do
it 'finds the master branch ref' do
expect(client.ref_exists?('refs/heads/master')).to eq(true)
end
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index b07462e4978..a6c99bc07d4 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -63,6 +63,45 @@ describe Gitlab::Gpg::Commit do
it_behaves_like 'returns the cached signature on second call'
end
+ context 'commit signed with a subkey' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User3.emails.first }
+
+ let!(:user) { create(:user, email: GpgHelpers::User3.emails.first) }
+
+ let!(:gpg_key) do
+ create :gpg_key, key: GpgHelpers::User3.public_key, user: user
+ end
+
+ let(:gpg_key_subkey) do
+ gpg_key.subkeys.find_by(fingerprint: '0522DD29B98F167CD8421752E38FFCAF75ABD92A')
+ end
+
+ before do
+ allow(Rugged::Commit).to receive(:extract_signature)
+ .with(Rugged::Repository, commit_sha)
+ .and_return(
+ [
+ GpgHelpers::User3.signed_commit_signature,
+ GpgHelpers::User3.signed_commit_base_data
+ ]
+ )
+ end
+
+ it 'returns a valid signature' do
+ expect(described_class.new(commit).signature).to have_attributes(
+ commit_sha: commit_sha,
+ project: project,
+ gpg_key: gpg_key_subkey,
+ gpg_key_primary_keyid: gpg_key_subkey.keyid,
+ gpg_key_user_name: GpgHelpers::User3.names.first,
+ gpg_key_user_email: GpgHelpers::User3.emails.first,
+ verification_status: 'verified'
+ )
+ end
+
+ it_behaves_like 'returns the cached signature on second call'
+ end
+
context 'user email does not match the committer email, but is the same user' do
let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first }
diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
index b9fd4d02156..d6000af0ecd 100644
--- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
+++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
@@ -2,17 +2,16 @@ require 'rails_helper'
RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
describe '#run' do
- let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
- let!(:project) { create :project, :repository, path: 'sample-project' }
+ let(:signature) { [GpgHelpers::User1.signed_commit_signature, GpgHelpers::User1.signed_commit_base_data] }
+ let(:committer_email) { GpgHelpers::User1.emails.first }
+ let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
+ let!(:project) { create :project, :repository, path: 'sample-project' }
let!(:raw_commit) do
raw_commit = double(
:raw_commit,
- signature: [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ],
+ signature: signature,
sha: commit_sha,
- committer_email: GpgHelpers::User1.emails.first
+ committer_email: committer_email
)
allow(raw_commit).to receive :save!
@@ -29,12 +28,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
allow(Rugged::Commit).to receive(:extract_signature)
.with(Rugged::Repository, commit_sha)
- .and_return(
- [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ]
- )
+ .and_return(signature)
end
context 'gpg signature did have an associated gpg key which was removed later' do
@@ -183,5 +177,34 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
)
end
end
+
+ context 'gpg signature did not have an associated gpg subkey' do
+ let(:signature) { [GpgHelpers::User3.signed_commit_signature, GpgHelpers::User3.signed_commit_base_data] }
+ let(:committer_email) { GpgHelpers::User3.emails.first }
+ let!(:user) { create :user, email: GpgHelpers::User3.emails.first }
+
+ let!(:invalid_gpg_signature) do
+ create :gpg_signature,
+ project: project,
+ commit_sha: commit_sha,
+ gpg_key: nil,
+ gpg_key_primary_keyid: GpgHelpers::User3.subkey_fingerprints.last[24..-1],
+ verification_status: 'unknown_key'
+ end
+
+ it 'updates the signature to being valid when the missing gpg key is added' do
+ # InvalidGpgSignatureUpdater is called by the after_create hook
+ gpg_key = create(:gpg_key, key: GpgHelpers::User3.public_key, user: user)
+ subkey = gpg_key.subkeys.last
+
+ expect(invalid_gpg_signature.reload).to have_attributes(
+ project: project,
+ commit_sha: commit_sha,
+ gpg_key_subkey_id: subkey.id,
+ gpg_key_primary_keyid: subkey.keyid,
+ verification_status: 'verified'
+ )
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb
index 11a2aea1915..ab9a166db00 100644
--- a/spec/lib/gitlab/gpg_spec.rb
+++ b/spec/lib/gitlab/gpg_spec.rb
@@ -28,6 +28,23 @@ describe Gitlab::Gpg do
end
end
+ describe '.subkeys_from_key' do
+ it 'returns the subkeys by primary key' do
+ all_subkeys = described_class.subkeys_from_key(GpgHelpers::User1.public_key)
+ subkeys = all_subkeys[GpgHelpers::User1.primary_keyid]
+
+ expect(subkeys).to be_present
+ expect(subkeys.first[:keyid]).to be_present
+ expect(subkeys.first[:fingerprint]).to be_present
+ end
+
+ it 'returns an empty array when there are not subkeys' do
+ all_subkeys = described_class.subkeys_from_key(GpgHelpers::User4.public_key)
+
+ expect(all_subkeys[GpgHelpers::User4.primary_keyid]).to be_empty
+ end
+ end
+
describe '.user_infos_from_key' do
it 'returns the names and emails' do
user_infos = described_class.user_infos_from_key(GpgHelpers::User1.public_key)
diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
index 73dd236a5c6..4c1ca4349ea 100644
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
describe '#readiness' do
subject { described_class.readiness }
- context 'storage has a tripped circuitbreaker', broken_storage: true do
+ context 'storage has a tripped circuitbreaker', :broken_storage do
let(:repository_storages) { ['broken'] }
let(:storages_paths) do
Gitlab.config.repositories.storages
diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
new file mode 100644
index 00000000000..30da56bec16
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe Gitlab::HookData::IssuableBuilder do
+ set(:user) { create(:user) }
+
+ # This shared example requires a `builder` and `user` variable
+ shared_examples 'issuable hook data' do |kind|
+ let(:data) { builder.build(user: user) }
+
+ include_examples 'project hook data' do
+ let(:project) { builder.issuable.project }
+ end
+ include_examples 'deprecated repository hook data'
+
+ context "with a #{kind}" do
+ it 'contains issuable data' do
+ expect(data[:object_kind]).to eq(kind)
+ expect(data[:user]).to eq(user.hook_attrs)
+ expect(data[:project]).to eq(builder.issuable.project.hook_attrs)
+ expect(data[:object_attributes]).to eq(builder.issuable.hook_attrs)
+ expect(data[:changes]).to eq({})
+ expect(data[:repository]).to eq(builder.issuable.project.hook_attrs.slice(:name, :url, :description, :homepage))
+ end
+
+ it 'does not contain certain keys' do
+ expect(data).not_to have_key(:assignees)
+ expect(data).not_to have_key(:assignee)
+ end
+
+ describe 'changes are given' do
+ let(:changes) do
+ {
+ cached_markdown_version: %w[foo bar],
+ description: ['A description', 'A cool description'],
+ description_html: %w[foo bar],
+ in_progress_merge_commit_sha: %w[foo bar],
+ lock_version: %w[foo bar],
+ merge_jid: %w[foo bar],
+ title: ['A title', 'Hello World'],
+ title_html: %w[foo bar],
+ labels: [
+ [{ id: 1, title: 'foo' }],
+ [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }]
+ ]
+ }
+ end
+ let(:data) { builder.build(user: user, changes: changes) }
+
+ it 'populates the :changes hash' do
+ expect(data[:changes]).to match(hash_including({
+ title: { previous: 'A title', current: 'Hello World' },
+ description: { previous: 'A description', current: 'A cool description' },
+ labels: {
+ previous: [{ id: 1, title: 'foo' }],
+ current: [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }]
+ }
+ }))
+ end
+
+ it 'does not contain certain keys' do
+ expect(data[:changes]).not_to have_key('cached_markdown_version')
+ expect(data[:changes]).not_to have_key('description_html')
+ expect(data[:changes]).not_to have_key('lock_version')
+ expect(data[:changes]).not_to have_key('title_html')
+ expect(data[:changes]).not_to have_key('in_progress_merge_commit_sha')
+ expect(data[:changes]).not_to have_key('merge_jid')
+ end
+ end
+ end
+ end
+
+ describe '#build' do
+ it_behaves_like 'issuable hook data', 'issue' do
+ let(:issuable) { create(:issue, description: 'A description') }
+ let(:builder) { described_class.new(issuable) }
+ end
+
+ it_behaves_like 'issuable hook data', 'merge_request' do
+ let(:issuable) { create(:merge_request, description: 'A description') }
+ let(:builder) { described_class.new(issuable) }
+ end
+
+ context 'issue is assigned' do
+ let(:issue) { create(:issue, assignees: [user]) }
+ let(:data) { described_class.new(issue).build(user: user) }
+
+ it 'returns correct hook data' do
+ expect(data[:object_attributes]['assignee_id']).to eq(user.id)
+ expect(data[:assignees].first).to eq(user.hook_attrs)
+ expect(data).not_to have_key(:assignee)
+ end
+ end
+
+ context 'merge_request is assigned' do
+ let(:merge_request) { create(:merge_request, assignee: user) }
+ let(:data) { described_class.new(merge_request).build(user: user) }
+
+ it 'returns correct hook data' do
+ expect(data[:object_attributes]['assignee_id']).to eq(user.id)
+ expect(data[:assignee]).to eq(user.hook_attrs)
+ expect(data).not_to have_key(:assignees)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
new file mode 100644
index 00000000000..6c529cdd051
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::HookData::IssueBuilder do
+ set(:issue) { create(:issue) }
+ let(:builder) { described_class.new(issue) }
+
+ describe '#build' do
+ let(:data) { builder.build }
+
+ it 'includes safe attribute' do
+ %w[
+ assignee_id
+ author_id
+ branch_name
+ closed_at
+ confidential
+ created_at
+ deleted_at
+ description
+ due_date
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ milestone_id
+ moved_to_id
+ project_id
+ relative_position
+ state
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].each do |key|
+ expect(data).to include(key)
+ end
+ end
+
+ it 'includes additional attrs' do
+ expect(data).to include(:total_time_spent)
+ expect(data).to include(:human_time_estimate)
+ expect(data).to include(:human_total_time_spent)
+ expect(data).to include(:assignee_ids)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
new file mode 100644
index 00000000000..92bf87bbad4
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::HookData::MergeRequestBuilder do
+ set(:merge_request) { create(:merge_request) }
+ let(:builder) { described_class.new(merge_request) }
+
+ describe '#build' do
+ let(:data) { builder.build }
+
+ it 'includes safe attribute' do
+ %w[
+ assignee_id
+ author_id
+ created_at
+ deleted_at
+ description
+ head_pipeline_id
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ merge_commit_sha
+ merge_error
+ merge_params
+ merge_status
+ merge_user_id
+ merge_when_pipeline_succeeds
+ milestone_id
+ ref_fetched
+ source_branch
+ source_project_id
+ state
+ target_branch
+ target_project_id
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].each do |key|
+ expect(data).to include(key)
+ end
+ end
+
+ %i[source target].each do |key|
+ describe "#{key} key" do
+ include_examples 'project hook data', project_key: key do
+ let(:project) { merge_request.public_send("#{key}_project") }
+ end
+ end
+ end
+
+ it 'includes additional attrs' do
+ expect(data).to include(:source)
+ expect(data).to include(:target)
+ expect(data).to include(:last_commit)
+ expect(data).to include(:work_in_progress)
+ expect(data).to include(:total_time_spent)
+ expect(data).to include(:human_time_estimate)
+ expect(data).to include(:human_total_time_spent)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 3fb8edeb701..29baa70d5ae 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -147,6 +147,10 @@ deploy_keys:
- user
- deploy_keys_projects
- projects
+cluster:
+- project
+- user
+- service
services:
- project
- service_hook
@@ -177,6 +181,7 @@ project:
- tag_taggings
- tags
- chat_services
+- cluster
- creator
- group
- namespace
@@ -266,6 +271,10 @@ project:
- container_repositories
- uploads
- members_and_requesters
+- build_trace_section_names
+- root_of_fork_network
+- fork_network_member
+- fork_network
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index c7fbc2bc92f..dd0ce0dae41 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -1,13 +1,15 @@
require 'spec_helper'
describe 'forked project import' do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') }
let!(:project) { create(:project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) }
let(:forked_from_project) { create(:project, :repository) }
- let(:fork_link) { create(:forked_project_link, forked_from_project: project_with_repo) }
+ let(:forked_project) { fork_project(project_with_repo, nil, repository: true) }
let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
@@ -16,7 +18,7 @@ describe 'forked project import' do
end
let!(:merge_request) do
- create(:merge_request, source_project: fork_link.forked_to_project, target_project: project_with_repo)
+ create(:merge_request, source_project: forked_project, target_project: project_with_repo)
end
let(:saver) do
diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
index 4d87f27ce05..473ba40fae7 100644
--- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
+++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
@@ -1,13 +1,14 @@
require 'spec_helper'
describe Gitlab::ImportExport::MergeRequestParser do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let!(:project) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') }
- let(:forked_from_project) { create(:project, :repository) }
- let(:fork_link) { create(:forked_project_link, forked_from_project: project) }
+ let(:forked_project) { fork_project(project) }
let!(:merge_request) do
- create(:merge_request, source_project: fork_link.forked_to_project, target_project: project)
+ create(:merge_request, source_project: forked_project, target_project: project)
end
let(:parsed_merge_request) do
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 7268226112c..80d92b2e6a3 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -25,6 +25,7 @@ Issue:
- relative_position
- last_edited_at
- last_edited_by_id
+- discussion_locked
Event:
- id
- target_type
@@ -168,6 +169,7 @@ MergeRequest:
- last_edited_at
- last_edited_by_id
- head_pipeline_id
+- discussion_locked
MergeRequestDiff:
- id
- state
@@ -224,6 +226,7 @@ Ci::Pipeline:
- auto_canceled_by_id
- pipeline_schedule_id
- config_source
+- failure_reason
- protected
Ci::Stage:
- id
@@ -310,6 +313,32 @@ Ci::PipelineSchedule:
- deleted_at
- created_at
- updated_at
+Gcp::Cluster:
+- id
+- project_id
+- user_id
+- service_id
+- enabled
+- status
+- status_reason
+- project_namespace
+- endpoint
+- ca_cert
+- encrypted_kubernetes_token
+- encrypted_kubernetes_token_iv
+- username
+- encrypted_password
+- encrypted_password_iv
+- gcp_project_id
+- gcp_cluster_zone
+- gcp_cluster_name
+- gcp_cluster_size
+- gcp_machine_type
+- gcp_operation_id
+- encrypted_gcp_token
+- encrypted_gcp_token_iv
+- created_at
+- updated_at
DeployKey:
- id
- user_id
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
index 8370adf9211..1785094af10 100644
--- a/spec/lib/gitlab/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::LDAP::AuthHash do
let(:auth_hash) do
described_class.new(
OmniAuth::AuthHash.new(
- uid: '123456',
+ uid: given_uid,
provider: 'ldapmain',
info: info,
extra: {
@@ -32,6 +32,8 @@ describe Gitlab::LDAP::AuthHash do
end
context "without overridden attributes" do
+ let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' }
+
it "has the correct username" do
expect(auth_hash.username).to eq("123456")
end
@@ -42,6 +44,8 @@ describe Gitlab::LDAP::AuthHash do
end
context "with overridden attributes" do
+ let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' }
+
let(:attributes) do
{
'username' => %w(mail email),
@@ -61,4 +65,22 @@ describe Gitlab::LDAP::AuthHash do
expect(auth_hash.name).to eq("John Smith")
end
end
+
+ describe '#uid' do
+ context 'when there is extraneous (but valid) whitespace' do
+ let(:given_uid) { 'uid =john smith , ou = people, dc= example,dc =com' }
+
+ it 'removes the extraneous whitespace' do
+ expect(auth_hash.uid).to eq('uid=john smith,ou=people,dc=example,dc=com')
+ end
+ end
+
+ context 'when there are upper case characters' do
+ let(:given_uid) { 'UID=John Smith,ou=People,dc=example,dc=com' }
+
+ it 'downcases' do
+ expect(auth_hash.uid).to eq('uid=john smith,ou=people,dc=example,dc=com')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb
new file mode 100644
index 00000000000..8e21ecdf9ab
--- /dev/null
+++ b/spec/lib/gitlab/ldap/dn_spec.rb
@@ -0,0 +1,224 @@
+require 'spec_helper'
+
+describe Gitlab::LDAP::DN do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#normalize_value' do
+ subject { described_class.normalize_value(given) }
+
+ it_behaves_like 'normalizes a DN attribute value'
+
+ context 'when the given DN is malformed' do
+ context 'when ending with a comma' do
+ let(:given) { 'John Smith,' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a space in it' do
+ let(:given) { '#aa aa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '#aaXaaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '#aaaYaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"")
+ end
+ end
+
+ context 'when given a hex pair with a non-hex character in it, inside double quotes' do
+ let(:given) { '"Sebasti\\cX\\a1n"' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"")
+ end
+ end
+
+ context 'with an open (as opposed to closed) double quote' do
+ let(:given) { '"James' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an invalid escaped hex code' do
+ let(:given) { 'J\ames' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"')
+ end
+ end
+
+ context 'with a value ending with the escape character' do
+ let(:given) { 'foo\\' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+ end
+ end
+
+ describe '#to_normalized_s' do
+ subject { described_class.new(given).to_normalized_s }
+
+ it_behaves_like 'normalizes a DN'
+
+ context 'when we do not support the given DN format' do
+ context 'multivalued RDNs' do
+ context 'without extraneous whitespace' do
+ let(:given) { 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' }
+
+ it 'raises UnsupportedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError)
+ end
+ end
+
+ context 'with extraneous whitespace' do
+ context 'around the phone number plus sign' do
+ let(:given) { 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' }
+
+ it 'raises UnsupportedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError)
+ end
+ end
+
+ context 'not around the phone number plus sign' do
+ let(:given) { 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' }
+
+ it 'raises UnsupportedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when the given DN is malformed' do
+ context 'when ending with a comma' do
+ let(:given) { 'uid=John Smith,' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a space in it' do
+ let(:given) { '0.9.2342.19200300.100.1.25=#aa aa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '0.9.2342.19200300.100.1.25=#aaXaaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '0.9.2342.19200300.100.1.25=#aaaYaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"")
+ end
+ end
+
+ context 'when given a hex pair with a non-hex character in it, inside double quotes' do
+ let(:given) { 'uid="Sebasti\\cX\\a1n"' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"")
+ end
+ end
+
+ context 'without a name value pair' do
+ let(:given) { 'John' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an open (as opposed to closed) double quote' do
+ let(:given) { 'cn="James' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an invalid escaped hex code' do
+ let(:given) { 'cn=J\ames' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"')
+ end
+ end
+
+ context 'with a value ending with the escape character' do
+ let(:given) { 'cn=\\' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an invalid OID attribute type name' do
+ let(:given) { '1.2.d=Value' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN OID attribute type name character "d"')
+ end
+ end
+
+ context 'with a period in a non-OID attribute type name' do
+ let(:given) { 'd1.2=Value' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "."')
+ end
+ end
+
+ context 'when starting with non-space, non-alphanumeric character' do
+ let(:given) { ' -uid=John Smith' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized first character of an RDN attribute type name "-"')
+ end
+ end
+
+ context 'when given a UID with an escaped equal sign' do
+ let(:given) { 'uid\\=john' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "\\"')
+ end
+ end
+ end
+ end
+
+ def assert_generic_test(test_description, got, expected)
+ test_failure_message = "Failed test description: '#{test_description}'\n\n expected: \"#{expected}\"\n got: \"#{got}\""
+ expect(got).to eq(expected), test_failure_message
+ end
+end
diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb
index 087c4d8c92c..d204050ef66 100644
--- a/spec/lib/gitlab/ldap/person_spec.rb
+++ b/spec/lib/gitlab/ldap/person_spec.rb
@@ -16,6 +16,34 @@ describe Gitlab::LDAP::Person do
)
end
+ describe '.normalize_dn' do
+ subject { described_class.normalize_dn(given) }
+
+ it_behaves_like 'normalizes a DN'
+
+ context 'with an exception during normalization' do
+ let(:given) { 'John "Smith,' } # just something that will cause an exception
+
+ it 'returns the given DN unmodified' do
+ expect(subject).to eq(given)
+ end
+ end
+ end
+
+ describe '.normalize_uid' do
+ subject { described_class.normalize_uid(given) }
+
+ it_behaves_like 'normalizes a DN attribute value'
+
+ context 'with an exception during normalization' do
+ let(:given) { 'John "Smith,' } # just something that will cause an exception
+
+ it 'returns the given UID unmodified' do
+ expect(subject).to eq(given)
+ end
+ end
+ end
+
describe '#name' do
it 'uses the configured name attribute and handles values as an array' do
name = 'John Doe'
@@ -43,4 +71,9 @@ describe Gitlab::LDAP::Person do
expect(person.email).to eq([user_principal_name])
end
end
+
+ def assert_generic_test(test_description, got, expected)
+ test_failure_message = "Failed test description: '#{test_description}'\n\n expected: #{expected}\n got: #{got}"
+ expect(got).to eq(expected), test_failure_message
+ end
end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 6a6e465cea2..9a4705d1cee 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::LDAP::User do
}
end
let(:auth_hash) do
- OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info)
+ OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info)
end
let(:ldap_user_upper_case) { described_class.new(auth_hash_upper_case) }
let(:info_upper_case) do
@@ -22,12 +22,12 @@ describe Gitlab::LDAP::User do
}
end
let(:auth_hash_upper_case) do
- OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info_upper_case)
+ OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info_upper_case)
end
describe '#changed?' do
it "marks existing ldap user as changed" do
- create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
+ create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain')
expect(ldap_user.changed?).to be_truthy
end
@@ -37,7 +37,7 @@ describe Gitlab::LDAP::User do
end
it "does not mark existing ldap user as changed" do
- create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain')
+ create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
ldap_user.gl_user.user_synced_attributes_metadata(provider: 'ldapmain', email: true)
expect(ldap_user.changed?).to be_falsey
end
@@ -60,7 +60,7 @@ describe Gitlab::LDAP::User do
describe 'find or create' do
it "finds the user if already existing" do
- create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
+ create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain')
expect { ldap_user.save }.not_to change { User.count }
end
@@ -70,7 +70,7 @@ describe Gitlab::LDAP::User do
expect { ldap_user.save }.not_to change { User.count }
existing_user.reload
- expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
+ expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
expect(existing_user.ldap_identity.provider).to eql 'ldapmain'
end
@@ -79,7 +79,7 @@ describe Gitlab::LDAP::User do
expect { ldap_user.save }.not_to change { User.count }
existing_user.reload
- expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
+ expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
expect(existing_user.ldap_identity.provider).to eql 'ldapmain'
expect(existing_user.id).to eql ldap_user.gl_user.id
end
@@ -89,7 +89,7 @@ describe Gitlab::LDAP::User do
expect { ldap_user_upper_case.save }.not_to change { User.count }
existing_user.reload
- expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
+ expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
expect(existing_user.ldap_identity.provider).to eql 'ldapmain'
expect(existing_user.id).to eql ldap_user.gl_user.id
end
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
new file mode 100644
index 00000000000..742a792a1af
--- /dev/null
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -0,0 +1,142 @@
+require 'spec_helper'
+
+describe Gitlab::Middleware::ReadOnly do
+ include Rack::Test::Methods
+
+ RSpec::Matchers.define :be_a_redirect do
+ match do |response|
+ response.status == 301
+ end
+ end
+
+ RSpec::Matchers.define :disallow_request do
+ match do |middleware|
+ flash = middleware.send(:rack_flash)
+ flash['alert'] && flash['alert'].include?('You cannot do writing operations')
+ end
+ end
+
+ RSpec::Matchers.define :disallow_request_in_json do
+ match do |response|
+ json_response = JSON.parse(response.body)
+ response.body.include?('You cannot do writing operations') && json_response.key?('message')
+ end
+ end
+
+ let(:rack_stack) do
+ rack = Rack::Builder.new do
+ use ActionDispatch::Session::CacheStore
+ use ActionDispatch::Flash
+ use ActionDispatch::ParamsParser
+ end
+
+ rack.run(subject)
+ rack.to_app
+ end
+
+ subject { described_class.new(fake_app) }
+
+ let(:request) { Rack::MockRequest.new(rack_stack) }
+
+ context 'normal requests to a read-only Gitlab instance' do
+ let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } }
+
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ it 'expects PATCH requests to be disallowed' do
+ response = request.patch('/test_request')
+
+ expect(response).to be_a_redirect
+ expect(subject).to disallow_request
+ end
+
+ it 'expects PUT requests to be disallowed' do
+ response = request.put('/test_request')
+
+ expect(response).to be_a_redirect
+ expect(subject).to disallow_request
+ end
+
+ it 'expects POST requests to be disallowed' do
+ response = request.post('/test_request')
+
+ expect(response).to be_a_redirect
+ expect(subject).to disallow_request
+ end
+
+ it 'expects a internal POST request to be allowed after a disallowed request' do
+ response = request.post('/test_request')
+
+ expect(response).to be_a_redirect
+
+ response = request.post("/api/#{API::API.version}/internal")
+
+ expect(response).not_to be_a_redirect
+ end
+
+ it 'expects DELETE requests to be disallowed' do
+ response = request.delete('/test_request')
+
+ expect(response).to be_a_redirect
+ expect(subject).to disallow_request
+ end
+
+ context 'whitelisted requests' do
+ it 'expects DELETE request to logout to be allowed' do
+ response = request.delete('/users/sign_out')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
+
+ it 'expects a POST internal request to be allowed' do
+ response = request.post("/api/#{API::API.version}/internal")
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
+
+ it 'expects a POST LFS request to batch URL to be allowed' do
+ response = request.post('/root/rouge.git/info/lfs/objects/batch')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
+ end
+ end
+
+ context 'json requests to a read-only GitLab instance' do
+ let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'application/json' }, ['OK']] } }
+ let(:content_json) { { 'CONTENT_TYPE' => 'application/json' } }
+
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ it 'expects PATCH requests to be disallowed' do
+ response = request.patch('/test_request', content_json)
+
+ expect(response).to disallow_request_in_json
+ end
+
+ it 'expects PUT requests to be disallowed' do
+ response = request.put('/test_request', content_json)
+
+ expect(response).to disallow_request_in_json
+ end
+
+ it 'expects POST requests to be disallowed' do
+ response = request.post('/test_request', content_json)
+
+ expect(response).to disallow_request_in_json
+ end
+
+ it 'expects DELETE requests to be disallowed' do
+ response = request.delete('/test_request', content_json)
+
+ expect(response).to disallow_request_in_json
+ end
+ end
+end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 4c5efbde69a..e44a7c23452 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::SearchResults do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let!(:project) { create(:project, name: 'foo') }
let!(:issue) { create(:issue, project: project, title: 'foo') }
@@ -42,7 +44,7 @@ describe Gitlab::SearchResults do
end
it 'includes merge requests from source and target projects' do
- forked_project = create(:project, forked_from_project: project)
+ forked_project = fork_project(project, user)
merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo')
results = described_class.new(user, Project.where(id: forked_project.id), 'foo')
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 9efdd7940ca..2158b2837e2 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -15,10 +15,6 @@ describe Gitlab::Shell do
it { is_expected.to respond_to :add_repository }
it { is_expected.to respond_to :remove_repository }
it { is_expected.to respond_to :fork_repository }
- it { is_expected.to respond_to :add_namespace }
- it { is_expected.to respond_to :rm_namespace }
- it { is_expected.to respond_to :mv_namespace }
- it { is_expected.to respond_to :exists? }
it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") }
@@ -160,7 +156,7 @@ describe Gitlab::Shell do
it_behaves_like '#add_repository'
end
- context 'without gitaly', skip_gitaly_mock: true do
+ context 'without gitaly', :skip_gitaly_mock do
it_behaves_like '#add_repository'
end
end
@@ -337,7 +333,7 @@ describe Gitlab::Shell do
end
end
- describe '#fetch_remote local', skip_gitaly_mock: true do
+ describe '#fetch_remote local', :skip_gitaly_mock do
it_should_behave_like 'fetch_remote', false
end
@@ -363,4 +359,52 @@ describe Gitlab::Shell do
end
end
end
+
+ describe 'namespace actions' do
+ subject { described_class.new }
+ let(:storage_path) { Gitlab.config.repositories.storages.default.path }
+
+ describe '#add_namespace' do
+ it 'creates a namespace' do
+ subject.add_namespace(storage_path, "mepmep")
+
+ expect(subject.exists?(storage_path, "mepmep")).to be(true)
+ end
+ end
+
+ describe '#exists?' do
+ context 'when the namespace does not exist' do
+ it 'returns false' do
+ expect(subject.exists?(storage_path, "non-existing")).to be(false)
+ end
+ end
+
+ context 'when the namespace exists' do
+ it 'returns true' do
+ subject.add_namespace(storage_path, "mepmep")
+
+ expect(subject.exists?(storage_path, "mepmep")).to be(true)
+ end
+ end
+ end
+
+ describe '#remove' do
+ it 'removes the namespace' do
+ subject.add_namespace(storage_path, "mepmep")
+ subject.rm_namespace(storage_path, "mepmep")
+
+ expect(subject.exists?(storage_path, "mepmep")).to be(false)
+ end
+ end
+
+ describe '#mv_namespace' do
+ it 'renames the namespace' do
+ subject.add_namespace(storage_path, "mepmep")
+ subject.mv_namespace(storage_path, "mepmep", "2mep")
+
+ expect(subject.exists?(storage_path, "mepmep")).to be(false)
+ expect(subject.exists?(storage_path, "2mep")).to be(true)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index ee152872acc..777e9c8e21d 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -60,6 +60,7 @@ describe Gitlab::UsageData do
deploy_keys
deployments
environments
+ gcp_clusters
in_review_folder
groups
issues
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 4dffe2bd82f..80bf7986ee0 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -41,7 +41,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_archive feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -66,12 +66,34 @@ describe Gitlab::Workhorse do
let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
subject { described_class.send_git_patch(repository, diff_refs) }
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ context 'when Gitaly workhorse_send_git_patch feature is enabled' do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-format-patch")
+ expect(params).to eq({
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'RawPatchRequest' => Gitaly::RawPatchRequest.new(
+ repository: repository.gitaly_repository,
+ left_commit_id: 'base',
+ right_commit_id: 'head'
+ ).to_json
+ }.deep_stringify_keys)
+ end
+ end
+
+ context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
- expect(key).to eq("Gitlab-Workhorse-Send-Data")
- expect(command).to eq("git-format-patch")
- expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-format-patch")
+ expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ end
end
end
@@ -115,14 +137,36 @@ describe Gitlab::Workhorse do
describe '.send_git_diff' do
let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
- subject { described_class.send_git_patch(repository, diff_refs) }
+ subject { described_class.send_git_diff(repository, diff_refs) }
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ context 'when Gitaly workhorse_send_git_diff feature is enabled' do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
- expect(key).to eq("Gitlab-Workhorse-Send-Data")
- expect(command).to eq("git-format-patch")
- expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-diff")
+ expect(params).to eq({
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'RawDiffRequest' => Gitaly::RawDiffRequest.new(
+ repository: repository.gitaly_repository,
+ left_commit_id: 'base',
+ right_commit_id: 'head'
+ ).to_json
+ }.deep_stringify_keys)
+ end
+ end
+
+ context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-diff")
+ expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ end
end
end
@@ -383,7 +427,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_raw_show feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
diff --git a/spec/lib/google_api/auth_spec.rb b/spec/lib/google_api/auth_spec.rb
new file mode 100644
index 00000000000..87a3f43274f
--- /dev/null
+++ b/spec/lib/google_api/auth_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe GoogleApi::Auth do
+ let(:redirect_uri) { 'http://localhost:3000/google_api/authorizations/callback' }
+ let(:redirect_to) { 'http://localhost:3000/namaspace/project/clusters' }
+
+ let(:client) do
+ GoogleApi::CloudPlatform::Client
+ .new(nil, redirect_uri, state: redirect_to)
+ end
+
+ describe '#authorize_url' do
+ subject { client.authorize_url }
+
+ it 'returns authorize_url' do
+ is_expected.to start_with('https://accounts.google.com/o/oauth2')
+ is_expected.to include(URI.encode(redirect_uri, URI::PATTERN::RESERVED))
+ is_expected.to include(URI.encode(redirect_to, URI::PATTERN::RESERVED))
+ end
+ end
+
+ describe '#get_token' do
+ let(:token) do
+ double.tap do |dbl|
+ allow(dbl).to receive(:token).and_return('token')
+ allow(dbl).to receive(:expires_at).and_return('expires_at')
+ end
+ end
+
+ before do
+ allow_any_instance_of(OAuth2::Strategy::AuthCode)
+ .to receive(:get_token).and_return(token)
+ end
+
+ it 'returns token and expires_at' do
+ token, expires_at = client.get_token('xxx')
+ expect(token).to eq('token')
+ expect(expires_at).to eq('expires_at')
+ end
+ end
+end
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
new file mode 100644
index 00000000000..acc5bd1da35
--- /dev/null
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -0,0 +1,128 @@
+require 'spec_helper'
+
+describe GoogleApi::CloudPlatform::Client do
+ let(:token) { 'token' }
+ let(:client) { described_class.new(token, nil) }
+
+ describe '.session_key_for_redirect_uri' do
+ let(:state) { 'random_string' }
+
+ subject { described_class.session_key_for_redirect_uri(state) }
+
+ it 'creates a new session key' do
+ is_expected.to eq('cloud_platform_second_redirect_uri_random_string')
+ end
+ end
+
+ describe '.new_session_key_for_redirect_uri' do
+ it 'generates a new session key' do
+ expect { |b| described_class.new_session_key_for_redirect_uri(&b) }
+ .to yield_with_args(String)
+ end
+ end
+
+ describe '#validate_token' do
+ subject { client.validate_token(expires_at) }
+
+ let(:expires_at) { 1.hour.since.utc.strftime('%s') }
+
+ context 'when token is nil' do
+ let(:token) { nil }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when expires_at is nil' do
+ let(:expires_at) { nil }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when expires in 1 hour' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when expires in 10 minutes' do
+ let(:expires_at) { 5.minutes.since.utc.strftime('%s') }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#projects_zones_clusters_get' do
+ subject { client.projects_zones_clusters_get(spy, spy, spy) }
+ let(:gke_cluster) { double }
+
+ before do
+ allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ .to receive(:get_zone_cluster).and_return(gke_cluster)
+ end
+
+ it { is_expected.to eq(gke_cluster) }
+ end
+
+ describe '#projects_zones_clusters_create' do
+ subject do
+ client.projects_zones_clusters_create(
+ spy, spy, cluster_name, cluster_size, machine_type: machine_type)
+ end
+
+ let(:cluster_name) { 'test-cluster' }
+ let(:cluster_size) { 1 }
+ let(:machine_type) { 'n1-standard-4' }
+ let(:operation) { double }
+
+ before do
+ allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ .to receive(:create_cluster).and_return(operation)
+ end
+
+ it { is_expected.to eq(operation) }
+
+ it 'sets corresponded parameters' do
+ expect_any_instance_of(Google::Apis::ContainerV1::CreateClusterRequest)
+ .to receive(:initialize).with(
+ {
+ "cluster": {
+ "name": cluster_name,
+ "initial_node_count": cluster_size,
+ "node_config": {
+ "machine_type": machine_type
+ }
+ }
+ } )
+
+ subject
+ end
+ end
+
+ describe '#projects_zones_operations' do
+ subject { client.projects_zones_operations(spy, spy, spy) }
+ let(:operation) { double }
+
+ before do
+ allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ .to receive(:get_zone_operation).and_return(operation)
+ end
+
+ it { is_expected.to eq(operation) }
+ end
+
+ describe '#parse_operation_id' do
+ subject { client.parse_operation_id(self_link) }
+
+ context 'when expected url' do
+ let(:self_link) do
+ 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123'
+ end
+
+ it { is_expected.to eq('ope-123') }
+ end
+
+ context 'when unexpected url' do
+ let(:self_link) { '???' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/lib/rspec_flaky/config_spec.rb b/spec/lib/rspec_flaky/config_spec.rb
new file mode 100644
index 00000000000..83556787e85
--- /dev/null
+++ b/spec/lib/rspec_flaky/config_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+describe RspecFlaky::Config, :aggregate_failures do
+ before do
+ # Stub these env variables otherwise specs don't behave the same on the CI
+ stub_env('FLAKY_RSPEC_GENERATE_REPORT', nil)
+ stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil)
+ stub_env('FLAKY_RSPEC_REPORT_PATH', nil)
+ stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', nil)
+ end
+
+ describe '.generate_report?' do
+ context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is not set" do
+ it 'returns false' do
+ expect(described_class).not_to be_generate_report
+ end
+ end
+
+ context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set to 'false'" do
+ before do
+ stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false')
+ end
+
+ it 'returns false' do
+ expect(described_class).not_to be_generate_report
+ end
+ end
+
+ context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set to 'true'" do
+ before do
+ stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true')
+ end
+
+ it 'returns true' do
+ expect(described_class).to be_generate_report
+ end
+ end
+ end
+
+ describe '.suite_flaky_examples_report_path' do
+ context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is not set" do
+ it 'returns the default path' do
+ expect(Rails.root).to receive(:join).with('rspec_flaky/suite-report.json')
+ .and_return('root/rspec_flaky/suite-report.json')
+
+ expect(described_class.suite_flaky_examples_report_path).to eq('root/rspec_flaky/suite-report.json')
+ end
+ end
+
+ context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is set" do
+ before do
+ stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', 'foo/suite-report.json')
+ end
+
+ it 'returns the value of the env variable' do
+ expect(described_class.suite_flaky_examples_report_path).to eq('foo/suite-report.json')
+ end
+ end
+ end
+
+ describe '.flaky_examples_report_path' do
+ context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is not set" do
+ it 'returns the default path' do
+ expect(Rails.root).to receive(:join).with('rspec_flaky/report.json')
+ .and_return('root/rspec_flaky/report.json')
+
+ expect(described_class.flaky_examples_report_path).to eq('root/rspec_flaky/report.json')
+ end
+ end
+
+ context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is set" do
+ before do
+ stub_env('FLAKY_RSPEC_REPORT_PATH', 'foo/report.json')
+ end
+
+ it 'returns the value of the env variable' do
+ expect(described_class.flaky_examples_report_path).to eq('foo/report.json')
+ end
+ end
+ end
+
+ describe '.new_flaky_examples_report_path' do
+ context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is not set" do
+ it 'returns the default path' do
+ expect(Rails.root).to receive(:join).with('rspec_flaky/new-report.json')
+ .and_return('root/rspec_flaky/new-report.json')
+
+ expect(described_class.new_flaky_examples_report_path).to eq('root/rspec_flaky/new-report.json')
+ end
+ end
+
+ context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is set" do
+ before do
+ stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', 'foo/new-report.json')
+ end
+
+ it 'returns the value of the env variable' do
+ expect(described_class.new_flaky_examples_report_path).to eq('foo/new-report.json')
+ end
+ end
+ end
+end
diff --git a/spec/lib/rspec_flaky/flaky_example_spec.rb b/spec/lib/rspec_flaky/flaky_example_spec.rb
index cbfc1e538ab..d19c34bebb3 100644
--- a/spec/lib/rspec_flaky/flaky_example_spec.rb
+++ b/spec/lib/rspec_flaky/flaky_example_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe RspecFlaky::FlakyExample do
+describe RspecFlaky::FlakyExample, :aggregate_failures do
let(:flaky_example_attrs) do
{
example_id: 'spec/foo/bar_spec.rb:2',
@@ -9,6 +9,7 @@ describe RspecFlaky::FlakyExample do
description: 'hello world',
first_flaky_at: 1234,
last_flaky_at: 2345,
+ last_flaky_job: 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/12',
last_attempts_count: 2,
flaky_reports: 1
}
@@ -27,57 +28,78 @@ describe RspecFlaky::FlakyExample do
end
let(:example) { double(example_attrs) }
+ before do
+ # Stub these env variables otherwise specs don't behave the same on the CI
+ stub_env('CI_PROJECT_URL', nil)
+ stub_env('CI_JOB_ID', nil)
+ end
+
describe '#initialize' do
shared_examples 'a valid FlakyExample instance' do
- it 'returns valid attributes' do
- flaky_example = described_class.new(args)
+ let(:flaky_example) { described_class.new(args) }
+ it 'returns valid attributes' do
expect(flaky_example.uid).to eq(flaky_example_attrs[:uid])
- expect(flaky_example.example_id).to eq(flaky_example_attrs[:example_id])
+ expect(flaky_example.file).to eq(flaky_example_attrs[:file])
+ expect(flaky_example.line).to eq(flaky_example_attrs[:line])
+ expect(flaky_example.description).to eq(flaky_example_attrs[:description])
+ expect(flaky_example.first_flaky_at).to eq(expected_first_flaky_at)
+ expect(flaky_example.last_flaky_at).to eq(expected_last_flaky_at)
+ expect(flaky_example.last_attempts_count).to eq(flaky_example_attrs[:last_attempts_count])
+ expect(flaky_example.flaky_reports).to eq(expected_flaky_reports)
end
end
context 'when given an Rspec::Example' do
- let(:args) { example }
-
- it_behaves_like 'a valid FlakyExample instance'
+ it_behaves_like 'a valid FlakyExample instance' do
+ let(:args) { example }
+ let(:expected_first_flaky_at) { nil }
+ let(:expected_last_flaky_at) { nil }
+ let(:expected_flaky_reports) { 0 }
+ end
end
context 'when given a hash' do
- let(:args) { flaky_example_attrs }
-
- it_behaves_like 'a valid FlakyExample instance'
+ it_behaves_like 'a valid FlakyExample instance' do
+ let(:args) { flaky_example_attrs }
+ let(:expected_flaky_reports) { flaky_example_attrs[:flaky_reports] }
+ let(:expected_first_flaky_at) { flaky_example_attrs[:first_flaky_at] }
+ let(:expected_last_flaky_at) { flaky_example_attrs[:last_flaky_at] }
+ end
end
end
- describe '#to_h' do
- before do
- # Stub these env variables otherwise specs don't behave the same on the CI
- stub_env('CI_PROJECT_URL', nil)
- stub_env('CI_JOB_ID', nil)
- end
+ describe '#update_flakiness!' do
+ shared_examples 'an up-to-date FlakyExample instance' do
+ let(:flaky_example) { described_class.new(args) }
- shared_examples 'a valid FlakyExample hash' do
- let(:additional_attrs) { {} }
+ it 'updates the first_flaky_at' do
+ now = Time.now
+ expected_first_flaky_at = flaky_example.first_flaky_at ? flaky_example.first_flaky_at : now
+ Timecop.freeze(now) { flaky_example.update_flakiness! }
- it 'returns a valid hash' do
- flaky_example = described_class.new(args)
- final_hash = flaky_example_attrs
- .merge(last_flaky_at: instance_of(Time), last_flaky_job: nil)
- .merge(additional_attrs)
+ expect(flaky_example.first_flaky_at).to eq(expected_first_flaky_at)
+ end
+
+ it 'updates the last_flaky_at' do
+ now = Time.now
+ Timecop.freeze(now) { flaky_example.update_flakiness! }
- expect(flaky_example.to_h).to match(hash_including(final_hash))
+ expect(flaky_example.last_flaky_at).to eq(now)
end
- end
- context 'when given an Rspec::Example' do
- let(:args) { example }
+ it 'updates the flaky_reports' do
+ expected_flaky_reports = flaky_example.first_flaky_at ? flaky_example.flaky_reports + 1 : 1
+
+ expect { flaky_example.update_flakiness! }.to change { flaky_example.flaky_reports }.by(1)
+ expect(flaky_example.flaky_reports).to eq(expected_flaky_reports)
+ end
+
+ context 'when passed a :last_attempts_count' do
+ it 'updates the last_attempts_count' do
+ flaky_example.update_flakiness!(last_attempts_count: 42)
- context 'when run locally' do
- it_behaves_like 'a valid FlakyExample hash' do
- let(:additional_attrs) do
- { first_flaky_at: instance_of(Time) }
- end
+ expect(flaky_example.last_attempts_count).to eq(42)
end
end
@@ -87,10 +109,45 @@ describe RspecFlaky::FlakyExample do
stub_env('CI_JOB_ID', 42)
end
- it_behaves_like 'a valid FlakyExample hash' do
- let(:additional_attrs) do
- { first_flaky_at: instance_of(Time), last_flaky_job: "https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/42" }
- end
+ it 'updates the last_flaky_job' do
+ flaky_example.update_flakiness!
+
+ expect(flaky_example.last_flaky_job).to eq('https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/42')
+ end
+ end
+ end
+
+ context 'when given an Rspec::Example' do
+ it_behaves_like 'an up-to-date FlakyExample instance' do
+ let(:args) { example }
+ end
+ end
+
+ context 'when given a hash' do
+ it_behaves_like 'an up-to-date FlakyExample instance' do
+ let(:args) { flaky_example_attrs }
+ end
+ end
+ end
+
+ describe '#to_h' do
+ shared_examples 'a valid FlakyExample hash' do
+ let(:additional_attrs) { {} }
+
+ it 'returns a valid hash' do
+ flaky_example = described_class.new(args)
+ final_hash = flaky_example_attrs.merge(additional_attrs)
+
+ expect(flaky_example.to_h).to eq(final_hash)
+ end
+ end
+
+ context 'when given an Rspec::Example' do
+ let(:args) { example }
+
+ it_behaves_like 'a valid FlakyExample hash' do
+ let(:additional_attrs) do
+ { first_flaky_at: nil, last_flaky_at: nil, last_flaky_job: nil, flaky_reports: 0 }
end
end
end
diff --git a/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb b/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb
new file mode 100644
index 00000000000..06a8ba0d02e
--- /dev/null
+++ b/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures do
+ let(:collection_hash) do
+ {
+ a: { example_id: 'spec/foo/bar_spec.rb:2' },
+ b: { example_id: 'spec/foo/baz_spec.rb:3' }
+ }
+ end
+ let(:collection_report) do
+ {
+ a: {
+ example_id: 'spec/foo/bar_spec.rb:2',
+ first_flaky_at: nil,
+ last_flaky_at: nil,
+ last_flaky_job: nil
+ },
+ b: {
+ example_id: 'spec/foo/baz_spec.rb:3',
+ first_flaky_at: nil,
+ last_flaky_at: nil,
+ last_flaky_job: nil
+ }
+ }
+ end
+
+ describe '.from_json' do
+ it 'accepts a JSON' do
+ collection = described_class.from_json(JSON.pretty_generate(collection_hash))
+
+ expect(collection.to_report).to eq(described_class.new(collection_hash).to_report)
+ end
+ end
+
+ describe '#initialize' do
+ it 'accepts no argument' do
+ expect { described_class.new }.not_to raise_error
+ end
+
+ it 'accepts a hash' do
+ expect { described_class.new(collection_hash) }.not_to raise_error
+ end
+
+ it 'does not accept anything else' do
+ expect { described_class.new([1, 2, 3]) }.to raise_error(ArgumentError, "`collection` must be a Hash, Array given!")
+ end
+ end
+
+ describe '#to_report' do
+ it 'calls #to_h on the values' do
+ collection = described_class.new(collection_hash)
+
+ expect(collection.to_report).to eq(collection_report)
+ end
+ end
+
+ describe '#-' do
+ it 'returns only examples that are not present in the given collection' do
+ collection1 = described_class.new(collection_hash)
+ collection2 = described_class.new(
+ a: { example_id: 'spec/foo/bar_spec.rb:2' },
+ c: { example_id: 'spec/bar/baz_spec.rb:4' })
+
+ expect((collection2 - collection1).to_report).to eq(
+ c: {
+ example_id: 'spec/bar/baz_spec.rb:4',
+ first_flaky_at: nil,
+ last_flaky_at: nil,
+ last_flaky_job: nil
+ })
+ end
+
+ it 'fails if the given collection does not respond to `#key?`' do
+ collection = described_class.new(collection_hash)
+
+ expect { collection - [1, 2, 3] }.to raise_error(ArgumentError, "`other` must respond to `#key?`, Array does not!")
+ end
+ end
+end
diff --git a/spec/lib/rspec_flaky/listener_spec.rb b/spec/lib/rspec_flaky/listener_spec.rb
index 0e193bf408b..bfb7648b486 100644
--- a/spec/lib/rspec_flaky/listener_spec.rb
+++ b/spec/lib/rspec_flaky/listener_spec.rb
@@ -1,22 +1,35 @@
require 'spec_helper'
-describe RspecFlaky::Listener do
- let(:flaky_example_report) do
+describe RspecFlaky::Listener, :aggregate_failures do
+ let(:already_flaky_example_uid) { '6e869794f4cfd2badd93eb68719371d1' }
+ let(:suite_flaky_example_report) do
{
- 'abc123' => {
+ already_flaky_example_uid => {
example_id: 'spec/foo/bar_spec.rb:2',
file: 'spec/foo/bar_spec.rb',
line: 2,
description: 'hello world',
first_flaky_at: 1234,
- last_flaky_at: instance_of(Time),
- last_attempts_count: 2,
+ last_flaky_at: 4321,
+ last_attempts_count: 3,
flaky_reports: 1,
last_flaky_job: nil
}
}
end
- let(:example_attrs) do
+ let(:already_flaky_example_attrs) do
+ {
+ id: 'spec/foo/bar_spec.rb:2',
+ metadata: {
+ file_path: 'spec/foo/bar_spec.rb',
+ line_number: 2,
+ full_description: 'hello world'
+ },
+ execution_result: double(status: 'passed', exception: nil)
+ }
+ end
+ let(:already_flaky_example) { RspecFlaky::FlakyExample.new(suite_flaky_example_report[already_flaky_example_uid]) }
+ let(:new_example_attrs) do
{
id: 'spec/foo/baz_spec.rb:3',
metadata: {
@@ -32,18 +45,19 @@ describe RspecFlaky::Listener do
# Stub these env variables otherwise specs don't behave the same on the CI
stub_env('CI_PROJECT_URL', nil)
stub_env('CI_JOB_ID', nil)
+ stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil)
end
describe '#initialize' do
shared_examples 'a valid Listener instance' do
- let(:expected_all_flaky_examples) { {} }
+ let(:expected_suite_flaky_examples) { {} }
it 'returns a valid Listener instance' do
listener = described_class.new
- expect(listener.to_report(listener.all_flaky_examples))
- .to match(hash_including(expected_all_flaky_examples))
- expect(listener.new_flaky_examples).to eq({})
+ expect(listener.to_report(listener.suite_flaky_examples))
+ .to eq(expected_suite_flaky_examples)
+ expect(listener.flaky_examples).to eq({})
end
end
@@ -51,16 +65,16 @@ describe RspecFlaky::Listener do
it_behaves_like 'a valid Listener instance'
end
- context 'when a report file exists and set by ALL_FLAKY_RSPEC_REPORT_PATH' do
+ context 'when a report file exists and set by SUITE_FLAKY_RSPEC_REPORT_PATH' do
let(:report_file) do
Tempfile.new(%w[rspec_flaky_report .json]).tap do |f|
- f.write(JSON.pretty_generate(flaky_example_report))
+ f.write(JSON.pretty_generate(suite_flaky_example_report))
f.rewind
end
end
before do
- stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file.path)
+ stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', report_file.path)
end
after do
@@ -69,74 +83,122 @@ describe RspecFlaky::Listener do
end
it_behaves_like 'a valid Listener instance' do
- let(:expected_all_flaky_examples) { flaky_example_report }
+ let(:expected_suite_flaky_examples) { suite_flaky_example_report }
end
end
end
describe '#example_passed' do
- let(:rspec_example) { double(example_attrs) }
+ let(:rspec_example) { double(new_example_attrs) }
let(:notification) { double(example: rspec_example) }
+ let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
shared_examples 'a non-flaky example' do
it 'does not change the flaky examples hash' do
- expect { subject.example_passed(notification) }
- .not_to change { subject.all_flaky_examples }
+ expect { listener.example_passed(notification) }
+ .not_to change { listener.flaky_examples }
end
end
- describe 'when the RSpec example does not respond to attempts' do
- it_behaves_like 'a non-flaky example'
- end
+ shared_examples 'an existing flaky example' do
+ let(:expected_flaky_example) do
+ {
+ example_id: 'spec/foo/bar_spec.rb:2',
+ file: 'spec/foo/bar_spec.rb',
+ line: 2,
+ description: 'hello world',
+ first_flaky_at: 1234,
+ last_attempts_count: 2,
+ flaky_reports: 2,
+ last_flaky_job: nil
+ }
+ end
- describe 'when the RSpec example has 1 attempt' do
- let(:rspec_example) { double(example_attrs.merge(attempts: 1)) }
+ it 'changes the flaky examples hash' do
+ new_example = RspecFlaky::Example.new(rspec_example)
- it_behaves_like 'a non-flaky example'
+ now = Time.now
+ Timecop.freeze(now) do
+ expect { listener.example_passed(notification) }
+ .to change { listener.flaky_examples[new_example.uid].to_h }
+ end
+
+ expect(listener.flaky_examples[new_example.uid].to_h)
+ .to eq(expected_flaky_example.merge(last_flaky_at: now))
+ end
end
- describe 'when the RSpec example has 2 attempts' do
- let(:rspec_example) { double(example_attrs.merge(attempts: 2)) }
- let(:expected_new_flaky_example) do
+ shared_examples 'a new flaky example' do
+ let(:expected_flaky_example) do
{
example_id: 'spec/foo/baz_spec.rb:3',
file: 'spec/foo/baz_spec.rb',
line: 3,
description: 'hello GitLab',
- first_flaky_at: instance_of(Time),
- last_flaky_at: instance_of(Time),
last_attempts_count: 2,
flaky_reports: 1,
last_flaky_job: nil
}
end
- it 'does not change the flaky examples hash' do
- expect { subject.example_passed(notification) }
- .to change { subject.all_flaky_examples }
-
+ it 'changes the all flaky examples hash' do
new_example = RspecFlaky::Example.new(rspec_example)
- expect(subject.all_flaky_examples[new_example.uid].to_h)
- .to match(hash_including(expected_new_flaky_example))
+ now = Time.now
+ Timecop.freeze(now) do
+ expect { listener.example_passed(notification) }
+ .to change { listener.flaky_examples[new_example.uid].to_h }
+ end
+
+ expect(listener.flaky_examples[new_example.uid].to_h)
+ .to eq(expected_flaky_example.merge(first_flaky_at: now, last_flaky_at: now))
+ end
+ end
+
+ describe 'when the RSpec example does not respond to attempts' do
+ it_behaves_like 'a non-flaky example'
+ end
+
+ describe 'when the RSpec example has 1 attempt' do
+ let(:rspec_example) { double(new_example_attrs.merge(attempts: 1)) }
+
+ it_behaves_like 'a non-flaky example'
+ end
+
+ describe 'when the RSpec example has 2 attempts' do
+ let(:rspec_example) { double(new_example_attrs.merge(attempts: 2)) }
+
+ it_behaves_like 'a new flaky example'
+
+ context 'with an existing flaky example' do
+ let(:rspec_example) { double(already_flaky_example_attrs.merge(attempts: 2)) }
+
+ it_behaves_like 'an existing flaky example'
end
end
end
describe '#dump_summary' do
- let(:rspec_example) { double(example_attrs) }
- let(:notification) { double(example: rspec_example) }
+ let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
+ let(:new_flaky_rspec_example) { double(new_example_attrs.merge(attempts: 2)) }
+ let(:already_flaky_rspec_example) { double(already_flaky_example_attrs.merge(attempts: 2)) }
+ let(:notification_new_flaky_rspec_example) { double(example: new_flaky_rspec_example) }
+ let(:notification_already_flaky_rspec_example) { double(example: already_flaky_rspec_example) }
- context 'when a report file path is set by ALL_FLAKY_RSPEC_REPORT_PATH' do
+ context 'when a report file path is set by FLAKY_RSPEC_REPORT_PATH' do
let(:report_file_path) { Rails.root.join('tmp', 'rspec_flaky_report.json') }
+ let(:new_report_file_path) { Rails.root.join('tmp', 'rspec_flaky_new_report.json') }
before do
- stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file_path)
+ stub_env('FLAKY_RSPEC_REPORT_PATH', report_file_path)
+ stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', new_report_file_path)
FileUtils.rm(report_file_path) if File.exist?(report_file_path)
+ FileUtils.rm(new_report_file_path) if File.exist?(new_report_file_path)
end
after do
FileUtils.rm(report_file_path) if File.exist?(report_file_path)
+ FileUtils.rm(new_report_file_path) if File.exist?(new_report_file_path)
end
context 'when FLAKY_RSPEC_GENERATE_REPORT == "false"' do
@@ -144,12 +206,13 @@ describe RspecFlaky::Listener do
stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false')
end
- it 'does not write the report file' do
- subject.example_passed(notification)
+ it 'does not write any report file' do
+ listener.example_passed(notification_new_flaky_rspec_example)
- subject.dump_summary(nil)
+ listener.dump_summary(nil)
expect(File.exist?(report_file_path)).to be(false)
+ expect(File.exist?(new_report_file_path)).to be(false)
end
end
@@ -158,21 +221,39 @@ describe RspecFlaky::Listener do
stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true')
end
- it 'writes the report file' do
- subject.example_passed(notification)
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ it 'writes the report files' do
+ listener.example_passed(notification_new_flaky_rspec_example)
+ listener.example_passed(notification_already_flaky_rspec_example)
- subject.dump_summary(nil)
+ listener.dump_summary(nil)
expect(File.exist?(report_file_path)).to be(true)
+ expect(File.exist?(new_report_file_path)).to be(true)
+
+ expect(File.read(report_file_path))
+ .to eq(JSON.pretty_generate(listener.to_report(listener.flaky_examples)))
+
+ new_example = RspecFlaky::Example.new(notification_new_flaky_rspec_example)
+ new_flaky_example = RspecFlaky::FlakyExample.new(new_example)
+ new_flaky_example.update_flakiness!
+
+ expect(File.read(new_report_file_path))
+ .to eq(JSON.pretty_generate(listener.to_report(new_example.uid => new_flaky_example)))
end
end
end
end
describe '#to_report' do
+ let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
+
it 'transforms the internal hash to a JSON-ready hash' do
- expect(subject.to_report('abc123' => RspecFlaky::FlakyExample.new(flaky_example_report['abc123'])))
- .to match(hash_including(flaky_example_report))
+ expect(listener.to_report(already_flaky_example_uid => already_flaky_example))
+ .to match(hash_including(suite_flaky_example_report))
end
end
end
diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
index a0fb86345f3..b4b83b70d1c 100644
--- a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
+++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
@@ -39,6 +39,14 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do
it { is_expected.to eq(expected_result) }
end
+
+ it 'skips GitLab read-only instances' do
+ stub_user
+ stub_home_dir
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+
+ is_expected.to be_truthy
+ end
end
describe '#check?' do
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 09e5094cf84..1f7be415e35 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -120,29 +120,4 @@ describe Emails::Profile do
it { expect { Notify.new_gpg_key_email('foo') }.not_to raise_error }
end
end
-
- describe 'user added email' do
- let(:email) { create(:email) }
-
- subject { Notify.new_email_email(email.id) }
-
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like 'a user cannot unsubscribe through footer link'
-
- it 'is sent to the new user' do
- is_expected.to deliver_to email.user.email
- end
-
- it 'has the correct subject' do
- is_expected.to have_subject /^Email was added to your account$/i
- end
-
- it 'contains the new email address' do
- is_expected.to have_body_text /#{email.email}/
- end
-
- it 'includes a link to emails page' do
- is_expected.to have_body_text /#{profile_emails_path}/
- end
- end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 932e2fd8c95..c832cee965b 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -28,319 +28,334 @@ describe Notify do
end
def have_referable_subject(referable, reply: false)
- prefix = referable.project.name if referable.project
- prefix = "Re: #{prefix}" if reply
+ prefix = referable.project ? "#{referable.project.name} | " : ''
+ prefix.prepend('Re: ') if reply
suffix = "#{referable.title} (#{referable.to_reference})"
- have_subject [prefix, suffix].compact.join(' | ')
+ have_subject [prefix, suffix].compact.join
end
context 'for a project' do
- describe 'items that are assignable, the email' do
- let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
+ shared_examples 'an assignee email' do
+ it 'is sent to the assignee as the author' do
+ sender = subject.header[:from].addrs.first
+
+ aggregate_failures do
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ expect(subject).to deliver_to(assignee.email)
+ end
+ end
+ end
- shared_examples 'an assignee email' do
- it 'is sent to the assignee as the author' do
- sender = subject.header[:from].addrs.first
+ context 'for issues' do
+ describe 'that are new' do
+ subject { described_class.new_issue_email(issue.assignees.first.id, issue.id) }
+ it_behaves_like 'an assignee email'
+ it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'has the correct subject and body' do
aggregate_failures do
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- expect(subject).to deliver_to(assignee.email)
+ is_expected.to have_referable_subject(issue)
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
end
- end
- context 'for issues' do
- describe 'that are new' do
- subject { described_class.new_issue_email(issue.assignees.first.id, issue.id) }
+ it 'contains the description' do
+ is_expected.to have_html_escaped_body_text issue.description
+ end
- it_behaves_like 'an assignee email'
- it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
- let(:model) { issue }
- end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
-
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue)
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
+ context 'when enabled email_author_in_body' do
+ before do
+ stub_application_setting(email_author_in_body: true)
end
- it 'contains the description' do
- is_expected.to have_html_escaped_body_text issue.description
+ it 'contains a link to note author' do
+ is_expected.to have_html_escaped_body_text(issue.author_name)
+ is_expected.to have_body_text 'created an issue:'
end
+ end
+ end
- context 'when enabled email_author_in_body' do
- before do
- stub_application_setting(email_author_in_body: true)
- end
+ describe 'that are reassigned' do
+ let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
+ subject { described_class.reassigned_issue_email(recipient.id, issue.id, [previous_assignee.id], current_user.id) }
- it 'contains a link to note author' do
- is_expected.to have_html_escaped_body_text(issue.author_name)
- is_expected.to have_body_text 'created an issue:'
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
- describe 'that have been reassigned' do
- subject { described_class.reassigned_issue_email(recipient.id, issue.id, [previous_assignee.id], current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_html_escaped_body_text(previous_assignee.name)
+ is_expected.to have_html_escaped_body_text(assignee.name)
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
+ end
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ describe 'that have been relabeled' do
+ subject { described_class.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_html_escaped_body_text(previous_assignee.name)
- is_expected.to have_html_escaped_body_text(assignee.name)
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email with a labels subscriptions link in its footer'
- describe 'that have been relabeled' do
- subject { described_class.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_body_text('foo, bar, and baz')
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'a user cannot unsubscribe through footer link'
- it_behaves_like 'an email with a labels subscriptions link in its footer'
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
+ context 'with a preferred language' do
+ before do
+ Gitlab::I18n.locale = :es
end
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_body_text('foo, bar, and baz')
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
+ after do
+ Gitlab::I18n.use_default_locale
end
- context 'with a preferred language' do
- before do
- Gitlab::I18n.locale = :es
- end
-
- after do
- Gitlab::I18n.use_default_locale
- end
-
- it 'always generates the email using the default language' do
- is_expected.to have_body_text('foo, bar, and baz')
- end
+ it 'always generates the email using the default language' do
+ is_expected.to have_body_text('foo, bar, and baz')
end
end
+ end
- describe 'status changed' do
- let(:status) { 'closed' }
- subject { described_class.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
+ describe 'status changed' do
+ let(:status) { 'closed' }
+ subject { described_class.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
- end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_body_text(status)
- is_expected.to have_html_escaped_body_text(current_user.name)
- is_expected.to have_body_text(project_issue_path project, issue)
- end
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_body_text(status)
+ is_expected.to have_html_escaped_body_text(current_user.name)
+ is_expected.to have_body_text(project_issue_path project, issue)
end
end
+ end
- describe 'moved to another project' do
- let(:new_issue) { create(:issue) }
- subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) }
+ describe 'moved to another project' do
+ let(:new_issue) { create(:issue) }
+ subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) }
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
- end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
- it 'contains description about action taken' do
- is_expected.to have_body_text 'Issue was moved to another project'
- end
+ it 'contains description about action taken' do
+ is_expected.to have_body_text 'Issue was moved to another project'
+ end
- it 'has the correct subject and body' do
- new_issue_url = project_issue_path(new_issue.project, new_issue)
+ it 'has the correct subject and body' do
+ new_issue_url = project_issue_path(new_issue.project, new_issue)
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_body_text(new_issue_url)
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_body_text(new_issue_url)
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
end
end
+ end
- context 'for merge requests' do
- describe 'that are new' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+ context 'for merge requests' do
+ describe 'that are new' do
+ subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+
+ it_behaves_like 'an assignee email'
+ it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
- it_behaves_like 'an assignee email'
- it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
- let(:model) { merge_request }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(merge_request)
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+ is_expected.to have_body_text(merge_request.source_branch)
+ is_expected.to have_body_text(merge_request.target_branch)
end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'an unsubscribeable thread'
-
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request)
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- is_expected.to have_body_text(merge_request.source_branch)
- is_expected.to have_body_text(merge_request.target_branch)
- end
+ end
+
+ it 'contains the description' do
+ is_expected.to have_html_escaped_body_text merge_request.description
+ end
+
+ context 'when enabled email_author_in_body' do
+ before do
+ stub_application_setting(email_author_in_body: true)
end
- it 'contains the description' do
- is_expected.to have_html_escaped_body_text merge_request.description
+ it 'contains a link to note author' do
+ is_expected.to have_html_escaped_body_text merge_request.author_name
+ is_expected.to have_body_text 'created a merge request:'
end
+ end
+ end
- context 'when enabled email_author_in_body' do
- before do
- stub_application_setting(email_author_in_body: true)
- end
+ describe 'that are reassigned' do
+ let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
+ subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
- it 'contains a link to note author' do
- is_expected.to have_html_escaped_body_text merge_request.author_name
- is_expected.to have_body_text 'created a merge request:'
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
- describe 'that are reassigned' do
- subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ is_expected.to have_html_escaped_body_text(previous_assignee.name)
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+ is_expected.to have_html_escaped_body_text(assignee.name)
end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like "an unsubscribeable thread"
+ end
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ describe 'that have been relabeled' do
+ subject { described_class.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_html_escaped_body_text(previous_assignee.name)
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- is_expected.to have_html_escaped_body_text(assignee.name)
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email with a labels subscriptions link in its footer'
- describe 'that have been relabeled' do
- subject { described_class.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
- end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'a user cannot unsubscribe through footer link'
- it_behaves_like 'an email with a labels subscriptions link in its footer'
+ it 'has the correct subject and body' do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ is_expected.to have_body_text('foo, bar, and baz')
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+ end
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ describe 'status changed' do
+ let(:status) { 'reopened' }
+ subject { described_class.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
- it 'has the correct subject and body' do
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'has the correct subject and body' do
+ aggregate_failures do
is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_body_text('foo, bar, and baz')
+ is_expected.to have_body_text(status)
+ is_expected.to have_html_escaped_body_text(current_user.name)
is_expected.to have_body_text(project_merge_request_path(project, merge_request))
end
end
+ end
- describe 'status changed' do
- let(:status) { 'reopened' }
- subject { described_class.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
+ describe 'that are merged' do
+ let(:merge_author) { create(:user) }
+ subject { described_class.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
- end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ it 'is sent as the merge author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(merge_author.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_body_text(status)
- is_expected.to have_html_escaped_body_text(current_user.name)
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- end
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ is_expected.to have_body_text('merged')
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
end
end
+ end
+ end
- describe 'that are merged' do
- let(:merge_author) { create(:user) }
- subject { described_class.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
+ context 'for snippet notes' do
+ let(:project_snippet) { create(:project_snippet, project: project) }
+ let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) }
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
- end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'an unsubscribeable thread'
+ subject { described_class.note_snippet_email(project_snippet_note.author_id, project_snippet_note.id) }
- it 'is sent as the merge author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(merge_author.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { project_snippet }
+ end
+ it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_body_text('merged')
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- end
- end
- end
+ it 'has the correct subject and body' do
+ is_expected.to have_referable_subject(project_snippet, reply: true)
+ is_expected.to have_html_escaped_body_text project_snippet_note.note
end
end
@@ -1239,4 +1254,18 @@ describe Notify do
end
end
end
+
+ context 'for personal snippet notes' do
+ let(:personal_snippet) { create(:personal_snippet) }
+ let(:personal_snippet_note) { create(:note_on_personal_snippet, noteable: personal_snippet) }
+
+ subject { described_class.note_personal_snippet_email(personal_snippet_note.author_id, personal_snippet_note.id) }
+
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'has the correct subject and body' do
+ is_expected.to have_referable_subject(personal_snippet, reply: true)
+ is_expected.to have_html_escaped_body_text personal_snippet_note.note
+ end
+ end
end
diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
index 862907c5d01..84c2e9f7e52 100644
--- a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
+++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
@@ -2,11 +2,12 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb')
describe AddHeadPipelineForEachMergeRequest, :truncate do
+ include ProjectForksHelper
+
let(:migration) { described_class.new }
let!(:project) { create(:project) }
- let!(:forked_project_link) { create(:forked_project_link, forked_from_project: project) }
- let!(:other_project) { forked_project_link.forked_to_project }
+ let!(:other_project) { fork_project(project) }
let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: "branch_1") }
let!(:pipeline_2) { create(:ci_pipeline, project: other_project, ref: "branch_1") }
diff --git a/spec/migrations/normalize_ldap_extern_uids_spec.rb b/spec/migrations/normalize_ldap_extern_uids_spec.rb
new file mode 100644
index 00000000000..262d7742aaf
--- /dev/null
+++ b/spec/migrations/normalize_ldap_extern_uids_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170921101004_normalize_ldap_extern_uids')
+
+describe NormalizeLdapExternUids, :migration, :sidekiq do
+ let!(:identities) { table(:identities) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_BATCH_SIZE", 2)
+ stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE", 2)
+
+ # LDAP identities
+ (1..4).each do |i|
+ identities.create!(id: i, provider: 'ldapmain', extern_uid: " uid = foo #{i}, ou = People, dc = example, dc = com ", user_id: i)
+ end
+
+ # Non-LDAP identity
+ identities.create!(id: 5, provider: 'foo', extern_uid: " uid = foo 5, ou = People, dc = example, dc = com ", user_id: 5)
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]])
+ expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(30.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+
+ it 'migrates the LDAP identities' do
+ Sidekiq::Testing.inline! do
+ migrate!
+ identities.where(id: 1..4).each do |identity|
+ expect(identity.extern_uid).to eq("uid=foo #{identity.id},ou=people,dc=example,dc=com")
+ end
+ end
+ end
+
+ it 'does not modify non-LDAP identities' do
+ Sidekiq::Testing.inline! do
+ migrate!
+ identity = identities.last
+ expect(identity.extern_uid).to eq(" uid = foo 5, ou = People, dc = example, dc = com ")
+ end
+ end
+end
diff --git a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb
new file mode 100644
index 00000000000..0e884a7d910
--- /dev/null
+++ b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys')
+
+describe ScheduleCreateGpgKeySubkeysFromGpgKeys, :migration, :sidekiq do
+ matcher :be_scheduled_migration do |*expected|
+ match do |migration|
+ BackgroundMigrationWorker.jobs.any? do |job|
+ job['args'] == [migration, expected]
+ end
+ end
+
+ failure_message do |migration|
+ "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
+ end
+ end
+
+ before do
+ create(:gpg_key, id: 1, key: GpgHelpers::User1.public_key)
+ create(:gpg_key, id: 2, key: GpgHelpers::User3.public_key)
+ # Delete all subkeys so they can be recreated
+ GpgKeySubkey.destroy_all
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_migration(1)
+ expect(described_class::MIGRATION).to be_scheduled_migration(2)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+
+ it 'schedules background migrations' do
+ Sidekiq::Testing.inline! do
+ expect(GpgKeySubkey.count).to eq(0)
+
+ migrate!
+
+ expect(GpgKeySubkey.count).to eq(3)
+ end
+ end
+end
diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb
index 0a45c5ea32d..d4a1553fb0e 100644
--- a/spec/migrations/update_upload_paths_to_system_spec.rb
+++ b/spec/migrations/update_upload_paths_to_system_spec.rb
@@ -31,7 +31,7 @@ describe UpdateUploadPathsToSystem do
end
end
- describe "#up", truncate: true do
+ describe "#up", :truncate do
it "updates old upload records to the new path" do
old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg")
@@ -41,7 +41,7 @@ describe UpdateUploadPathsToSystem do
end
end
- describe "#down", truncate: true do
+ describe "#down", :truncate do
it "updates the new system patsh to the old paths" do
new_upload = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg")
diff --git a/spec/models/ci/artifact_blob_spec.rb b/spec/models/ci/artifact_blob_spec.rb
index a10a8af5303..d5ba088af53 100644
--- a/spec/models/ci/artifact_blob_spec.rb
+++ b/spec/models/ci/artifact_blob_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Ci::ArtifactBlob do
- let(:build) { create(:ci_build, :artifacts) }
+ set(:project) { create(:project, :public) }
+ set(:build) { create(:ci_build, :artifacts, project: project) }
let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/another-subdirectory/banana_sample.gif') }
subject { described_class.new(entry) }
@@ -41,4 +42,51 @@ describe Ci::ArtifactBlob do
expect(subject.external_storage).to eq(:build_artifact)
end
end
+
+ describe '#external_url' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ context '.gif extension' do
+ it 'returns nil' do
+ expect(subject.external_url(build.project, build)).to be_nil
+ end
+ end
+
+ context 'txt extensions' do
+ let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') }
+
+ it 'returns a URL' do
+ url = subject.external_url(build.project, build)
+
+ expect(url).not_to be_nil
+ expect(url).to start_with("http")
+ expect(url).to match Gitlab.config.pages.host
+ expect(url).to end_with(entry.path)
+ end
+ end
+ end
+
+ describe '#external_link?' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ context 'gif extensions' do
+ it 'returns false' do
+ expect(subject.external_link?(build)).to be false
+ end
+ end
+
+ context 'txt extensions' do
+ let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') }
+
+ it 'returns true' do
+ expect(subject.external_link?(build)).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 451968c7342..06f76b5501e 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -18,6 +18,7 @@ describe Ci::Build do
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
it { is_expected.to have_many(:deployments) }
+ it { is_expected.to have_many(:trace_sections)}
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) }
@@ -320,6 +321,17 @@ describe Ci::Build do
end
end
+ describe '#parse_trace_sections!' do
+ it 'calls ExtractSectionsFromBuildTraceService' do
+ expect(Ci::ExtractSectionsFromBuildTraceService)
+ .to receive(:new).with(project, build.user).once.and_call_original
+ expect_any_instance_of(Ci::ExtractSectionsFromBuildTraceService)
+ .to receive(:execute).with(build).once
+
+ build.parse_trace_sections!
+ end
+ end
+
describe '#trace' do
subject { build.trace }
diff --git a/spec/models/ci/build_trace_section_name_spec.rb b/spec/models/ci/build_trace_section_name_spec.rb
new file mode 100644
index 00000000000..386ee6880cb
--- /dev/null
+++ b/spec/models/ci/build_trace_section_name_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceSectionName, model: true do
+ subject { build(:ci_build_trace_section_name) }
+
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:trace_sections)}
+
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
+end
diff --git a/spec/models/ci/build_trace_section_spec.rb b/spec/models/ci/build_trace_section_spec.rb
new file mode 100644
index 00000000000..541a9a36fb8
--- /dev/null
+++ b/spec/models/ci/build_trace_section_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceSection, model: true do
+ it { is_expected.to belong_to(:build)}
+ it { is_expected.to belong_to(:project)}
+ it { is_expected.to belong_to(:section_name)}
+
+ it { is_expected.to validate_presence_of(:section_name) }
+ it { is_expected.to validate_presence_of(:build) }
+ it { is_expected.to validate_presence_of(:project) }
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 9c1e460ab20..2c9e7013b77 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -238,7 +238,7 @@ describe Ci::Pipeline, :mailer do
describe '#stage_seeds' do
let(:pipeline) do
- create(:ci_pipeline, config: { rspec: { script: 'rake' } })
+ build(:ci_pipeline, config: { rspec: { script: 'rake' } })
end
it 'returns preseeded stage seeds object' do
@@ -247,6 +247,14 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#seeds_size' do
+ let(:pipeline) { build(:ci_pipeline_with_one_job) }
+
+ it 'returns number of jobs in stage seeds' do
+ expect(pipeline.seeds_size).to eq 1
+ end
+ end
+
describe '#legacy_stages' do
subject { pipeline.legacy_stages }
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 40bbb10eaac..129dfa07f15 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -178,57 +178,59 @@ describe CacheMarkdownField do
end
end
- describe '#refresh_markdown_cache!' do
+ describe '#refresh_markdown_cache' do
before do
thing.foo = updated_markdown
end
- context 'do_update: false' do
- it 'fills all html fields' do
- thing.refresh_markdown_cache!
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.foo_html_changed?).to be_truthy
- expect(thing.baz_html_changed?).to be_truthy
- end
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.foo_html_changed?).to be_truthy
+ expect(thing.baz_html_changed?).to be_truthy
+ end
- it 'does not save the result' do
- expect(thing).not_to receive(:update_columns)
+ it 'does not save the result' do
+ expect(thing).not_to receive(:update_columns)
- thing.refresh_markdown_cache!
- end
+ thing.refresh_markdown_cache
+ end
- it 'updates the markdown cache version' do
- thing.cached_markdown_version = nil
- thing.refresh_markdown_cache!
+ it 'updates the markdown cache version' do
+ thing.cached_markdown_version = nil
+ thing.refresh_markdown_cache
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
- end
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
end
+ end
- context 'do_update: true' do
- it 'fills all html fields' do
- thing.refresh_markdown_cache!(do_update: true)
+ describe '#refresh_markdown_cache!' do
+ before do
+ thing.foo = updated_markdown
+ end
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.foo_html_changed?).to be_truthy
- expect(thing.baz_html_changed?).to be_truthy
- end
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache!
- it 'skips saving if not persisted' do
- expect(thing).to receive(:persisted?).and_return(false)
- expect(thing).not_to receive(:update_columns)
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.foo_html_changed?).to be_truthy
+ expect(thing.baz_html_changed?).to be_truthy
+ end
- thing.refresh_markdown_cache!(do_update: true)
- end
+ it 'skips saving if not persisted' do
+ expect(thing).to receive(:persisted?).and_return(false)
+ expect(thing).not_to receive(:update_columns)
- it 'saves the changes using #update_columns' do
- expect(thing).to receive(:persisted?).and_return(true)
- expect(thing).to receive(:update_columns)
- .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
+ thing.refresh_markdown_cache!
+ end
- thing.refresh_markdown_cache!(do_update: true)
- end
+ it 'saves the changes using #update_columns' do
+ expect(thing).to receive(:persisted?).and_return(true)
+ expect(thing).to receive(:update_columns)
+ .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
+
+ thing.refresh_markdown_cache!
end
end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index a38f2553eb1..6866b43432c 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -231,6 +231,18 @@ describe HasStatus do
end
end
+ describe '.alive' do
+ subject { CommitStatus.alive }
+
+ %i[running pending created].each do |status|
+ it_behaves_like 'containing the job', status
+ end
+
+ %i[failed success].each do |status|
+ it_behaves_like 'not containing the job', status
+ end
+ end
+
describe '.created_or_pending' do
subject { CommitStatus.created_or_pending }
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index fb5fb7daaab..ba57301a3c9 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Issuable do
let(:issuable_class) { Issue }
- let(:issue) { create(:issue) }
+ let(:issue) { create(:issue, title: 'An issue', description: 'A description') }
let(:user) { create(:user) }
describe "Associations" do
@@ -264,55 +264,75 @@ describe Issuable do
end
end
- describe "#to_hook_data" do
- let(:data) { issue.to_hook_data(user) }
- let(:project) { issue.project }
-
- it "returns correct hook data" do
- expect(data[:object_kind]).to eq("issue")
- expect(data[:user]).to eq(user.hook_attrs)
- expect(data[:object_attributes]).to eq(issue.hook_attrs)
- expect(data).not_to have_key(:assignee)
- end
+ describe '#to_hook_data' do
+ context 'labels are updated' do
+ let(:labels) { create_list(:label, 2) }
- context "issue is assigned" do
before do
- issue.assignees << user
+ issue.update(labels: [labels[1]])
end
- it "returns correct hook data" do
- expect(data[:assignees].first).to eq(user.hook_attrs)
+ it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
+ builder = double
+
+ expect(Gitlab::HookData::IssuableBuilder)
+ .to receive(:new).with(issue).and_return(builder)
+ expect(builder).to receive(:build).with(
+ user: user,
+ changes: hash_including(
+ 'labels' => [[labels[0].hook_attrs], [labels[1].hook_attrs]]
+ ))
+
+ issue.to_hook_data(user, old_labels: [labels[0]])
end
end
- context "merge_request is assigned" do
- let(:merge_request) { create(:merge_request) }
- let(:data) { merge_request.to_hook_data(user) }
+ context 'issue is assigned' do
+ let(:user2) { create(:user) }
before do
- merge_request.update_attribute(:assignee, user)
+ issue.assignees << user << user2
end
- it "returns correct hook data" do
- expect(data[:object_attributes]['assignee_id']).to eq(user.id)
- expect(data[:assignee]).to eq(user.hook_attrs)
+ it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
+ builder = double
+
+ expect(Gitlab::HookData::IssuableBuilder)
+ .to receive(:new).with(issue).and_return(builder)
+ expect(builder).to receive(:build).with(
+ user: user,
+ changes: hash_including(
+ 'assignees' => [[user.hook_attrs], [user.hook_attrs, user2.hook_attrs]]
+ ))
+
+ issue.to_hook_data(user, old_assignees: [user])
end
end
- context 'issue has labels' do
- let(:labels) { [create(:label), create(:label)] }
+ context 'merge_request is assigned' do
+ let(:merge_request) { create(:merge_request) }
+ let(:user2) { create(:user) }
before do
- issue.update_attribute(:labels, labels)
+ merge_request.update(assignee: user)
+ merge_request.update(assignee: user2)
end
- it 'includes labels in the hook data' do
- expect(data[:labels]).to eq(labels.map(&:hook_attrs))
+ it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
+ builder = double
+
+ expect(Gitlab::HookData::IssuableBuilder)
+ .to receive(:new).with(merge_request).and_return(builder)
+ expect(builder).to receive(:build).with(
+ user: user,
+ changes: hash_including(
+ 'assignee_id' => [user.id, user2.id],
+ 'assignee' => [user.hook_attrs, user2.hook_attrs]
+ ))
+
+ merge_request.to_hook_data(user, old_assignees: [user])
end
end
-
- include_examples 'project hook data'
- include_examples 'deprecated repository hook data'
end
describe '#labels_array' do
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index b463d12e448..ab8773b7ede 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -12,6 +12,16 @@ describe Group, 'Routable' do
it { is_expected.to have_many(:redirect_routes).dependent(:destroy) }
end
+ describe 'GitLab read-only instance' do
+ it 'does not save route if route is not present' do
+ group.route.path = ''
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ expect(group).to receive(:update_route).and_call_original
+
+ expect { group.full_path }.to change { Route.count }.by(0)
+ end
+ end
+
describe 'Callbacks' do
it 'creates route record on create' do
expect(group.route.path).to eq(group.path)
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 4aa9ec789a3..eb0a3e9e0d3 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe DiffNote do
include RepoHelpers
- let(:merge_request) { create(:merge_request) }
+ let!(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:commit) { project.commit(sample_commit.id) }
@@ -98,14 +98,14 @@ describe DiffNote do
diff_line = subject.diff_line
expect(diff_line.added?).to be true
- expect(diff_line.new_line).to eq(position.new_line)
+ expect(diff_line.new_line).to eq(position.formatter.new_line)
expect(diff_line.text).to eq("+ vars = {")
end
end
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.new_line, 15)
+ line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.formatter.new_line, 15)
expect(subject.line_code).to eq(line_code)
end
@@ -255,4 +255,38 @@ describe DiffNote do
end
end
end
+
+ describe "image diff notes" do
+ let(:path) { "files/images/any_image.png" }
+
+ let!(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ width: 10,
+ height: 10,
+ x: 1,
+ y: 1,
+ diff_refs: merge_request.diff_refs,
+ position_type: "image"
+ )
+ end
+
+ describe "validations" do
+ subject { build(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
+
+ it { is_expected.not_to validate_presence_of(:line_code) }
+
+ it "does not validate diff line" do
+ diff_line = subject.diff_line
+
+ expect(diff_line).to be nil
+ expect(subject).to be_valid
+ end
+ end
+
+ it "returns true for on_image?" do
+ expect(subject.on_image?).to be_truthy
+ end
+ end
end
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index 1d6fabe48b1..b32dd31ae6d 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -11,4 +11,33 @@ describe Email do
expect(described_class.new(email: ' inFO@exAMPLe.com ').email)
.to eq 'info@example.com'
end
+
+ describe '#update_invalid_gpg_signatures' do
+ let(:user) do
+ create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
+ user.skip_reconfirmation!
+ end
+ end
+ let(:user) { create(:user) }
+
+ it 'synchronizes the gpg keys when the email is updated' do
+ email = user.emails.create(email: 'new@email.com')
+
+ expect(user).to receive(:update_invalid_gpg_signatures)
+
+ email.confirm
+ end
+ end
+
+ describe 'scopes' do
+ let(:user) { create(:user) }
+
+ it 'scopes confirmed emails' do
+ create(:email, :confirmed, user: user)
+ create(:email, user: user)
+
+ expect(user.emails.count).to eq 2
+ expect(user.emails.confirmed.count).to eq 1
+ end
+ end
end
diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb
new file mode 100644
index 00000000000..532ca1fca8c
--- /dev/null
+++ b/spec/models/fork_network_member_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+
+describe ForkNetworkMember do
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:fork_network) }
+ end
+end
diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb
new file mode 100644
index 00000000000..605ccd6db06
--- /dev/null
+++ b/spec/models/fork_network_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe ForkNetwork do
+ include ProjectForksHelper
+
+ describe '#add_root_as_member' do
+ it 'adds the root project as a member when creating a new root network' do
+ project = create(:project)
+ fork_network = described_class.create(root_project: project)
+
+ expect(fork_network.projects).to include(project)
+ end
+ end
+
+ describe '#find_fork_in' do
+ it 'finds all fork of the current network in al collection' do
+ network = create(:fork_network)
+ root_project = network.root_project
+ another_project = fork_project(root_project)
+ create(:project)
+
+ expect(network.find_forks_in(Project.all))
+ .to contain_exactly(another_project, root_project)
+ end
+ end
+
+ context 'for a deleted project' do
+ it 'keeps the fork network' do
+ project = create(:project, :public)
+ forked = fork_project(project)
+ project.destroy!
+
+ fork_network = forked.reload.fork_network
+
+ expect(fork_network.projects).to contain_exactly(forked)
+ expect(fork_network.root_project).to be_nil
+ end
+
+ it 'allows multiple fork networks where the root project is deleted' do
+ first_project = create(:project)
+ second_project = create(:project)
+ first_fork = fork_project(first_project)
+ second_fork = fork_project(second_project)
+
+ first_project.destroy
+ second_project.destroy
+
+ expect(first_fork.fork_network).not_to be_nil
+ expect(first_fork.fork_network.root_project).to be_nil
+ expect(second_fork.fork_network).not_to be_nil
+ expect(second_fork.fork_network.root_project).to be_nil
+ end
+ end
+end
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index 7dbeb4d2e74..32e33e8f42f 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -1,10 +1,11 @@
require 'spec_helper'
describe ForkedProjectLink, "add link on fork" do
+ include ProjectForksHelper
+
let(:project_from) { create(:project, :repository) }
let(:project_to) { fork_project(project_from, user) }
let(:user) { create(:user) }
- let(:namespace) { user.namespace }
before do
project_from.add_reporter(user)
@@ -64,13 +65,4 @@ describe ForkedProjectLink, "add link on fork" do
expect(ForkedProjectLink.exists?(id: forked_project_link.id)).to eq(false)
end
end
-
- def fork_project(from_project, user)
- service = Projects::ForkService.new(from_project, user)
- shell = double('gitlab_shell', fork_repository: true)
-
- allow(service).to receive(:gitlab_shell).and_return(shell)
-
- service.execute
- end
end
diff --git a/spec/models/gcp/cluster_spec.rb b/spec/models/gcp/cluster_spec.rb
new file mode 100644
index 00000000000..350fbc257d9
--- /dev/null
+++ b/spec/models/gcp/cluster_spec.rb
@@ -0,0 +1,240 @@
+require 'spec_helper'
+
+describe Gcp::Cluster do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:service) }
+
+ it { is_expected.to validate_presence_of(:gcp_cluster_zone) }
+
+ describe '#default_value_for' do
+ let(:cluster) { described_class.new }
+
+ it { expect(cluster.gcp_cluster_zone).to eq('us-central1-a') }
+ it { expect(cluster.gcp_cluster_size).to eq(3) }
+ it { expect(cluster.gcp_machine_type).to eq('n1-standard-4') }
+ end
+
+ describe '#validates' do
+ subject { cluster.valid? }
+
+ context 'when validates gcp_project_id' do
+ let(:cluster) { build(:gcp_cluster, gcp_project_id: gcp_project_id) }
+
+ context 'when valid' do
+ let(:gcp_project_id) { 'gcp-project-12345' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when empty' do
+ let(:gcp_project_id) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when too long' do
+ let(:gcp_project_id) { 'A' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when includes abnormal character' do
+ let(:gcp_project_id) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates gcp_cluster_name' do
+ let(:cluster) { build(:gcp_cluster, gcp_cluster_name: gcp_cluster_name) }
+
+ context 'when valid' do
+ let(:gcp_cluster_name) { 'test-cluster' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when empty' do
+ let(:gcp_cluster_name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when too long' do
+ let(:gcp_cluster_name) { 'A' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when includes abnormal character' do
+ let(:gcp_cluster_name) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates gcp_cluster_size' do
+ let(:cluster) { build(:gcp_cluster, gcp_cluster_size: gcp_cluster_size) }
+
+ context 'when valid' do
+ let(:gcp_cluster_size) { 1 }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when zero' do
+ let(:gcp_cluster_size) { 0 }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates project_namespace' do
+ let(:cluster) { build(:gcp_cluster, project_namespace: project_namespace) }
+
+ context 'when valid' do
+ let(:project_namespace) { 'default-namespace' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when empty' do
+ let(:project_namespace) { '' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when too long' do
+ let(:project_namespace) { 'A' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when includes abnormal character' do
+ let(:project_namespace) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates restrict_modification' do
+ let(:cluster) { create(:gcp_cluster) }
+
+ before do
+ cluster.make_creating!
+ end
+
+ context 'when created' do
+ before do
+ cluster.make_created!
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when creating' do
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#state_machine' do
+ let(:cluster) { build(:gcp_cluster) }
+
+ context 'when transits to created state' do
+ before do
+ cluster.gcp_token = 'tmp'
+ cluster.gcp_operation_id = 'tmp'
+ cluster.make_created!
+ end
+
+ it 'nullify gcp_token and gcp_operation_id' do
+ expect(cluster.gcp_token).to be_nil
+ expect(cluster.gcp_operation_id).to be_nil
+ expect(cluster).to be_created
+ end
+ end
+
+ context 'when transits to errored state' do
+ let(:reason) { 'something wrong' }
+
+ before do
+ cluster.make_errored!(reason)
+ end
+
+ it 'sets status_reason' do
+ expect(cluster.status_reason).to eq(reason)
+ expect(cluster).to be_errored
+ end
+ end
+ end
+
+ describe '#project_namespace_placeholder' do
+ subject { cluster.project_namespace_placeholder }
+
+ let(:cluster) { create(:gcp_cluster) }
+
+ it 'returns a placeholder' do
+ is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}")
+ end
+ end
+
+ describe '#on_creation?' do
+ subject { cluster.on_creation? }
+
+ let(:cluster) { create(:gcp_cluster) }
+
+ context 'when status is creating' do
+ before do
+ cluster.make_creating!
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when status is created' do
+ before do
+ cluster.make_created!
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#api_url' do
+ subject { cluster.api_url }
+
+ let(:cluster) { create(:gcp_cluster, :created_on_gke) }
+ let(:api_url) { 'https://' + cluster.endpoint }
+
+ it { is_expected.to eq(api_url) }
+ end
+
+ describe '#restrict_modification' do
+ subject { cluster.restrict_modification }
+
+ let(:cluster) { create(:gcp_cluster) }
+
+ context 'when status is created' do
+ before do
+ cluster.make_created!
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when status is creating' do
+ before do
+ cluster.make_creating!
+ end
+
+ it { is_expected.to be_falsey }
+
+ it 'sets error' do
+ is_expected.to be_falsey
+ expect(cluster.errors).not_to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb
index 4a4d079b721..33e6f1de3d1 100644
--- a/spec/models/gpg_key_spec.rb
+++ b/spec/models/gpg_key_spec.rb
@@ -3,6 +3,7 @@ require 'rails_helper'
describe GpgKey do
describe "associations" do
it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:subkeys) }
end
describe "validation" do
@@ -38,6 +39,14 @@ describe GpgKey do
expect(gpg_key.primary_keyid).to eq GpgHelpers::User1.primary_keyid
end
end
+
+ describe 'generate_subkeys' do
+ it 'extracts the subkeys from the gpg key' do
+ gpg_key = create(:gpg_key, key: GpgHelpers::User1.public_key_with_extra_signing_key)
+
+ expect(gpg_key.subkeys.count).to eq(2)
+ end
+ end
end
describe '#key=' do
@@ -90,11 +99,20 @@ describe GpgKey do
it 'email is verified if the user has the matching email' do
user = create :user, email: 'bette.cartwright@example.com'
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
+ create :email, user: user
+ user.reload
expect(gpg_key.emails_with_verified_status).to eq(
'bette.cartwright@example.com' => true,
'bette.cartwright@example.net' => false
)
+
+ create :email, :confirmed, user: user, email: 'bette.cartwright@example.net'
+ user.reload
+ expect(gpg_key.emails_with_verified_status).to eq(
+ 'bette.cartwright@example.com' => true,
+ 'bette.cartwright@example.net' => true
+ )
end
end
@@ -173,5 +191,29 @@ describe GpgKey do
expect(unrelated_gpg_key.destroyed?).to be false
end
+
+ it 'deletes all the associated subkeys' do
+ gpg_key = create :gpg_key, key: GpgHelpers::User3.public_key
+
+ expect(gpg_key.subkeys).to be_present
+
+ gpg_key.revoke
+
+ expect(gpg_key.subkeys(true)).to be_blank
+ end
+
+ it 'invalidates all signatures associated to the subkeys' do
+ gpg_key = create :gpg_key, key: GpgHelpers::User3.public_key
+ gpg_key_subkey = gpg_key.subkeys.last
+ gpg_signature = create :gpg_signature, verification_status: :verified, gpg_key: gpg_key_subkey
+
+ gpg_key.revoke
+
+ expect(gpg_signature.reload).to have_attributes(
+ verification_status: 'unknown_key',
+ gpg_key: nil,
+ gpg_key_subkey: nil
+ )
+ end
end
end
diff --git a/spec/models/gpg_key_subkey_spec.rb b/spec/models/gpg_key_subkey_spec.rb
new file mode 100644
index 00000000000..3c86837f47f
--- /dev/null
+++ b/spec/models/gpg_key_subkey_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+describe GpgKeySubkey do
+ subject { build(:gpg_key_subkey) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:gpg_key) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:gpg_key_id) }
+ it { is_expected.to validate_presence_of(:fingerprint) }
+ it { is_expected.to validate_presence_of(:keyid) }
+ end
+end
diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb
index c58fd46762a..0136bb61c07 100644
--- a/spec/models/gpg_signature_spec.rb
+++ b/spec/models/gpg_signature_spec.rb
@@ -1,9 +1,17 @@
require 'rails_helper'
RSpec.describe GpgSignature do
+ let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
+ let!(:project) { create(:project, :repository, path: 'sample-project') }
+ let!(:commit) { create(:commit, project: project, sha: commit_sha) }
+ let(:gpg_signature) { create(:gpg_signature, commit_sha: commit_sha) }
+ let(:gpg_key) { create(:gpg_key) }
+ let(:gpg_key_subkey) { create(:gpg_key_subkey) }
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:gpg_key) }
+ it { is_expected.to belong_to(:gpg_key_subkey) }
end
describe 'validation' do
@@ -15,14 +23,48 @@ RSpec.describe GpgSignature do
describe '#commit' do
it 'fetches the commit through the project' do
- commit_sha = '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
- project = create :project, :repository
- commit = create :commit, project: project
- gpg_signature = create :gpg_signature, commit_sha: commit_sha
-
expect_any_instance_of(Project).to receive(:commit).with(commit_sha).and_return(commit)
gpg_signature.commit
end
end
+
+ describe '#gpg_key=' do
+ it 'supports the assignment of a GpgKey' do
+ gpg_signature = create(:gpg_signature, gpg_key: gpg_key)
+
+ expect(gpg_signature.gpg_key).to be_an_instance_of(GpgKey)
+ end
+
+ it 'supports the assignment of a GpgKeySubkey' do
+ gpg_signature = create(:gpg_signature, gpg_key: gpg_key_subkey)
+
+ expect(gpg_signature.gpg_key).to be_an_instance_of(GpgKeySubkey)
+ end
+
+ it 'clears gpg_key and gpg_key_subkey_id when passing nil' do
+ gpg_signature.update_attribute(:gpg_key, nil)
+
+ expect(gpg_signature.gpg_key_id).to be_nil
+ expect(gpg_signature.gpg_key_subkey_id).to be_nil
+ end
+ end
+
+ describe '#gpg_commit' do
+ context 'when commit does not exist' do
+ it 'returns nil' do
+ allow(gpg_signature).to receive(:commit).and_return(nil)
+
+ expect(gpg_signature.gpg_commit).to be_nil
+ end
+ end
+
+ context 'when commit exists' do
+ it 'returns an instance of Gitlab::Gpg::Commit' do
+ allow(gpg_signature).to receive(:commit).and_return(commit)
+
+ expect(gpg_signature.gpg_commit).to be_an_instance_of(Gitlab::Gpg::Commit)
+ end
+ end
+ end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index e547da0cfbe..bb5033c1628 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -700,18 +700,14 @@ describe Issue do
end
describe '#hook_attrs' do
- let(:attrs_hash) { subject.hook_attrs }
+ it 'delegates to Gitlab::HookData::IssueBuilder#build' do
+ builder = double
- it 'includes time tracking attrs' do
- expect(attrs_hash).to include(:total_time_spent)
- expect(attrs_hash).to include(:human_time_estimate)
- expect(attrs_hash).to include(:human_total_time_spent)
- expect(attrs_hash).to include('time_estimate')
- end
+ expect(Gitlab::HookData::IssueBuilder)
+ .to receive(:new).with(subject).and_return(builder)
+ expect(builder).to receive(:build)
- it 'includes assignee_ids and deprecated assignee_id' do
- expect(attrs_hash).to include(:assignee_id)
- expect(attrs_hash).to include(:assignee_ids)
+ subject.hook_attrs
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index a07ce05a865..0a017c068ad 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -488,7 +488,7 @@ describe Member do
member.accept_invite!(user)
end
- it "refreshes user's authorized projects", truncate: true do
+ it "refreshes user's authorized projects", :truncate do
project = member.source
expect(user.authorized_projects).not_to include(project)
@@ -523,7 +523,7 @@ describe Member do
end
end
- describe "destroying a record", truncate: true do
+ describe "destroying a record", :truncate do
it "refreshes user's authorized projects" do
project = create(:project, :private)
user = create(:user)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 188a0a98ec3..17c9f15b021 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe MergeRequest do
include RepoHelpers
+ include ProjectForksHelper
subject { create(:merge_request) }
@@ -49,6 +50,19 @@ describe MergeRequest do
expect(subject).to be_valid
end
end
+
+ context 'for forks' do
+ let(:project) { create(:project) }
+ let(:fork1) { fork_project(project) }
+ let(:fork2) { fork_project(project) }
+
+ it 'allows merge requests for sibling-forks' do
+ subject.source_project = fork1
+ subject.target_project = fork2
+
+ expect(subject).to be_valid
+ end
+ end
end
describe 'respond to' do
@@ -646,33 +660,21 @@ describe MergeRequest do
end
end
- describe "#hook_attrs" do
- let(:attrs_hash) { subject.hook_attrs }
+ describe '#hook_attrs' do
+ it 'delegates to Gitlab::HookData::MergeRequestBuilder#build' do
+ builder = double
- [:source, :target].each do |key|
- describe "#{key} key" do
- include_examples 'project hook data', project_key: key do
- let(:data) { attrs_hash }
- let(:project) { subject.send("#{key}_project") }
- end
- end
- end
+ expect(Gitlab::HookData::MergeRequestBuilder)
+ .to receive(:new).with(subject).and_return(builder)
+ expect(builder).to receive(:build)
- it "has all the required keys" do
- expect(attrs_hash).to include(:source)
- expect(attrs_hash).to include(:target)
- expect(attrs_hash).to include(:last_commit)
- expect(attrs_hash).to include(:work_in_progress)
- expect(attrs_hash).to include(:total_time_spent)
- expect(attrs_hash).to include(:human_time_estimate)
- expect(attrs_hash).to include(:human_total_time_spent)
- expect(attrs_hash).to include('time_estimate')
+ subject.hook_attrs
end
end
describe '#diverged_commits_count' do
let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:project, :repository, forked_from_project: project) }
+ let(:forked_project) { fork_project(project, nil, repository: true) }
context 'when the target branch does not exist anymore' do
subject { create(:merge_request, source_project: project, target_project: project) }
@@ -700,7 +702,7 @@ describe MergeRequest do
end
context 'diverged on fork' do
- subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) }
+ subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: forked_project, target_project: project) }
it 'counts commits that are on target branch but not on source branch' do
expect(subject.diverged_commits_count).to eq(29)
@@ -708,7 +710,7 @@ describe MergeRequest do
end
context 'rebased on fork' do
- subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: fork_project, target_project: project) }
+ subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: forked_project, target_project: project) }
it 'counts commits that are on target branch but not on source branch' do
expect(subject.diverged_commits_count).to eq(0)
@@ -1257,11 +1259,7 @@ describe MergeRequest do
end
context 'with environments on source project' do
- let(:source_project) do
- create(:project, :repository) do |fork_project|
- fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- end
- end
+ let(:source_project) { fork_project(project, nil, repository: true) }
let(:merge_request) do
create(:merge_request,
@@ -1425,14 +1423,14 @@ describe MergeRequest do
describe "#source_project_missing?" do
let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:forked_project) { fork_project(project) }
let(:user) { create(:user) }
- let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
context "when the fork exists" do
let(:merge_request) do
create(:merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
@@ -1446,9 +1444,9 @@ describe MergeRequest do
end
context "when the fork does not exist" do
- let(:merge_request) do
+ let!(:merge_request) do
create(:merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
@@ -1471,14 +1469,14 @@ describe MergeRequest do
describe "#closed_without_fork?" do
let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:forked_project) { fork_project(project) }
let(:user) { create(:user) }
- let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
context "when the merge request is closed" do
let(:closed_merge_request) do
create(:closed_merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
@@ -1497,7 +1495,7 @@ describe MergeRequest do
context "when the merge request is open" do
let(:open_merge_request) do
create(:merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
@@ -1516,24 +1514,24 @@ describe MergeRequest do
end
context 'forked project' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
let(:user) { create(:user) }
- let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
+ let(:forked_project) { fork_project(project, user) }
let!(:merge_request) do
create(:closed_merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project)
end
it 'returns false if unforked' do
- Projects::UnlinkForkService.new(fork_project, user).execute
+ Projects::UnlinkForkService.new(forked_project, user).execute
expect(merge_request.reload.reopenable?).to be_falsey
end
it 'returns false if the source project is deleted' do
- Projects::DestroyService.new(fork_project, user).execute
+ Projects::DestroyService.new(forked_project, user).execute
expect(merge_request.reload.reopenable?).to be_falsey
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 4ce9f1b02e3..1bd8e8a5415 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Namespace do
+ include ProjectForksHelper
+
let!(:namespace) { create(:namespace) }
describe 'associations' do
@@ -166,22 +168,24 @@ describe Namespace do
end
describe '#move_dir' do
+ let(:namespace) { create(:namespace) }
+ let!(:project) { create(:project_empty_repo, namespace: namespace) }
+
before do
- @namespace = create :namespace
- @project = create(:project_empty_repo, namespace: @namespace)
- allow(@namespace).to receive(:path_changed?).and_return(true)
+ allow(namespace).to receive(:path_changed?).and_return(true)
end
it "raises error when directory exists" do
- expect { @namespace.move_dir }.to raise_error("namespace directory cannot be moved")
+ expect { namespace.move_dir }.to raise_error("namespace directory cannot be moved")
end
it "moves dir if path changed" do
- new_path = @namespace.full_path + "_new"
- allow(@namespace).to receive(:full_path_was).and_return(@namespace.full_path)
- allow(@namespace).to receive(:full_path).and_return(new_path)
- expect(@namespace).to receive(:remove_exports!)
- expect(@namespace.move_dir).to be_truthy
+ new_path = namespace.full_path + "_new"
+
+ allow(namespace).to receive(:full_path_was).and_return(namespace.full_path)
+ allow(namespace).to receive(:full_path).and_return(new_path)
+ expect(namespace).to receive(:remove_exports!)
+ expect(namespace.move_dir).to be_truthy
end
context "when any project has container images" do
@@ -191,14 +195,14 @@ describe Namespace do
stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: :any, tags: ['tag'])
- create(:project, namespace: @namespace, container_repositories: [container_repository])
+ create(:project, namespace: namespace, container_repositories: [container_repository])
- allow(@namespace).to receive(:path_was).and_return(@namespace.path)
- allow(@namespace).to receive(:path).and_return('new_path')
+ allow(namespace).to receive(:path_was).and_return(namespace.path)
+ allow(namespace).to receive(:path).and_return('new_path')
end
it 'raises an error about not movable project' do
- expect { @namespace.move_dir }.to raise_error(/Namespace cannot be moved/)
+ expect { namespace.move_dir }.to raise_error(/Namespace cannot be moved/)
end
end
@@ -532,4 +536,25 @@ describe Namespace do
end
end
end
+
+ describe '#has_forks_of?' do
+ let(:project) { create(:project, :public) }
+ let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) }
+
+ before do
+ # Reset the fork network relation
+ project.reload
+ end
+
+ it 'knows if there is a direct fork in the namespace' do
+ expect(namespace.find_fork_of(project)).to eq(forked_project)
+ end
+
+ it 'knows when there is as fork-of-fork in the namespace' do
+ other_namespace = create(:namespace)
+ other_fork = fork_project(forked_project, other_namespace.owner, namespace: other_namespace)
+
+ expect(other_namespace.find_fork_of(project)).to eq(other_fork)
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index b214074fdce..1ecb50586c7 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -314,6 +314,56 @@ describe Note do
expect(subject[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id)
expect(subject[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id)
end
+
+ context 'with image discussions' do
+ let(:merge_request2) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, title: "Added images and changes") }
+ let(:image_path) { "files/images/ee_repo_logo.png" }
+ let(:text_path) { "bar/branch-test.txt" }
+ let!(:image_note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: image_position) }
+ let!(:text_note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: text_position) }
+
+ let(:image_position) do
+ Gitlab::Diff::Position.new(
+ old_path: image_path,
+ new_path: image_path,
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 1,
+ position_type: "image",
+ diff_refs: merge_request2.diff_refs
+ )
+ end
+
+ let(:text_position) do
+ Gitlab::Diff::Position.new(
+ old_path: text_path,
+ new_path: text_path,
+ old_line: nil,
+ new_line: 2,
+ position_type: "text",
+ diff_refs: merge_request2.diff_refs
+ )
+ end
+
+ it "groups image discussions by file identifier" do
+ diff_discussion = DiffDiscussion.new([image_note])
+
+ discussions = merge_request2.notes.grouped_diff_discussions
+
+ expect(discussions.size).to eq(2)
+ expect(discussions[image_note.diff_file.new_path]).to include(diff_discussion)
+ end
+
+ it "groups text discussions by line code" do
+ diff_discussion = DiffDiscussion.new([text_note])
+
+ discussions = merge_request2.notes.grouped_diff_discussions
+
+ expect(discussions.size).to eq(2)
+ expect(discussions[text_note.line_code]).to include(diff_discussion)
+ end
+ end
end
context 'diff discussions for older diff refs' do
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index b3513c80150..41e2ab20d69 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -30,7 +30,7 @@ describe ProjectGroupLink do
end
end
- describe "destroying a record", truncate: true do
+ describe "destroying a record", :truncate do
it "refreshes group users' authorized projects" do
project = create(:project, :private)
group = create(:group)
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
index 4bb1db684e6..d37726dc3f1 100644
--- a/spec/models/project_services/chat_message/issue_message_spec.rb
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -42,7 +42,7 @@ describe ChatMessage::IssueMessage do
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
- '[<http://somewhere.com|project_name>] Issue opened by test.user')
+ '[<http://somewhere.com|project_name>] Issue opened by Test User (test.user)')
expect(subject.attachments).to eq([
{
title: "#100 Issue title",
@@ -62,7 +62,7 @@ describe ChatMessage::IssueMessage do
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
- '[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> closed by test.user')
+ '[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> closed by Test User (test.user)')
expect(subject.attachments).to be_empty
end
end
@@ -76,10 +76,10 @@ describe ChatMessage::IssueMessage do
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
- '[[project_name](http://somewhere.com)] Issue opened by test.user')
+ '[[project_name](http://somewhere.com)] Issue opened by Test User (test.user)')
expect(subject.attachments).to eq('issue description')
expect(subject.activity).to eq({
- title: 'Issue opened by test.user',
+ title: 'Issue opened by Test User (test.user)',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[#100 Issue title](http://url.com)',
image: 'http://someavatar.com'
@@ -95,10 +95,10 @@ describe ChatMessage::IssueMessage do
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
- '[[project_name](http://somewhere.com)] Issue [#100 Issue title](http://url.com) closed by test.user')
+ '[[project_name](http://somewhere.com)] Issue [#100 Issue title](http://url.com) closed by Test User (test.user)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
- title: 'Issue closed by test.user',
+ title: 'Issue closed by Test User (test.user)',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[#100 Issue title](http://url.com)',
image: 'http://someavatar.com'
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
index b600a36f578..184a07ae0f9 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -33,7 +33,7 @@ describe ChatMessage::MergeMessage do
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
- 'test.user opened <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
+ 'Test User (test.user) opened <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
expect(subject.attachments).to be_empty
end
end
@@ -44,7 +44,7 @@ describe ChatMessage::MergeMessage do
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
- 'test.user closed <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
+ 'Test User (test.user) closed <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
expect(subject.attachments).to be_empty
end
end
@@ -58,10 +58,10 @@ describe ChatMessage::MergeMessage do
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
- 'test.user opened [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
+ 'Test User (test.user) opened [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
- title: 'Merge Request opened by test.user',
+ title: 'Merge Request opened by Test User (test.user)',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[!100 *Merge Request title*](http://somewhere.com/merge_requests/100)',
image: 'http://someavatar.com'
@@ -76,10 +76,10 @@ describe ChatMessage::MergeMessage do
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
- 'test.user closed [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
+ 'Test User (test.user) closed [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
- title: 'Merge Request closed by test.user',
+ title: 'Merge Request closed by Test User (test.user)',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[!100 *Merge Request title*](http://somewhere.com/merge_requests/100)',
image: 'http://someavatar.com'
diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb
index a09c2f9935c..5abbd7bec18 100644
--- a/spec/models/project_services/chat_message/note_message_spec.rb
+++ b/spec/models/project_services/chat_message/note_message_spec.rb
@@ -38,7 +38,7 @@ describe ChatMessage::NoteMessage do
context 'without markdown' do
it 'returns a message regarding notes on commits' do
- expect(subject.pretext).to eq("test.user <http://url.com|commented on " \
+ expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \
"commit 5f163b2b> in <http://somewhere.com|project_name>: " \
"*Added a commit message*")
expect(subject.attachments).to eq([{
@@ -55,11 +55,11 @@ describe ChatMessage::NoteMessage do
it 'returns a message regarding notes on commits' do
expect(subject.pretext).to eq(
- 'test.user [commented on commit 5f163b2b](http://url.com) in [project_name](http://somewhere.com): *Added a commit message*'
+ 'Test User (test.user) [commented on commit 5f163b2b](http://url.com) in [project_name](http://somewhere.com): *Added a commit message*'
)
expect(subject.attachments).to eq('comment on a commit')
expect(subject.activity).to eq({
- title: 'test.user [commented on commit 5f163b2b](http://url.com)',
+ title: 'Test User (test.user) [commented on commit 5f163b2b](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'Added a commit message',
image: 'http://fakeavatar'
@@ -81,7 +81,7 @@ describe ChatMessage::NoteMessage do
context 'without markdown' do
it 'returns a message regarding notes on a merge request' do
- expect(subject.pretext).to eq("test.user <http://url.com|commented on " \
+ expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \
"merge request !30> in <http://somewhere.com|project_name>: " \
"*merge request title*")
expect(subject.attachments).to eq([{
@@ -98,10 +98,10 @@ describe ChatMessage::NoteMessage do
it 'returns a message regarding notes on a merge request' do
expect(subject.pretext).to eq(
- 'test.user [commented on merge request !30](http://url.com) in [project_name](http://somewhere.com): *merge request title*')
+ 'Test User (test.user) [commented on merge request !30](http://url.com) in [project_name](http://somewhere.com): *merge request title*')
expect(subject.attachments).to eq('comment on a merge request')
expect(subject.activity).to eq({
- title: 'test.user [commented on merge request !30](http://url.com)',
+ title: 'Test User (test.user) [commented on merge request !30](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'merge request title',
image: 'http://fakeavatar'
@@ -124,7 +124,7 @@ describe ChatMessage::NoteMessage do
context 'without markdown' do
it 'returns a message regarding notes on an issue' do
expect(subject.pretext).to eq(
- "test.user <http://url.com|commented on " \
+ "Test User (test.user) <http://url.com|commented on " \
"issue #20> in <http://somewhere.com|project_name>: " \
"*issue title*")
expect(subject.attachments).to eq([{
@@ -141,10 +141,10 @@ describe ChatMessage::NoteMessage do
it 'returns a message regarding notes on an issue' do
expect(subject.pretext).to eq(
- 'test.user [commented on issue #20](http://url.com) in [project_name](http://somewhere.com): *issue title*')
+ 'Test User (test.user) [commented on issue #20](http://url.com) in [project_name](http://somewhere.com): *issue title*')
expect(subject.attachments).to eq('comment on an issue')
expect(subject.activity).to eq({
- title: 'test.user [commented on issue #20](http://url.com)',
+ title: 'Test User (test.user) [commented on issue #20](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'issue title',
image: 'http://fakeavatar'
@@ -165,7 +165,7 @@ describe ChatMessage::NoteMessage do
context 'without markdown' do
it 'returns a message regarding notes on a project snippet' do
- expect(subject.pretext).to eq("test.user <http://url.com|commented on " \
+ expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \
"snippet $5> in <http://somewhere.com|project_name>: " \
"*snippet title*")
expect(subject.attachments).to eq([{
@@ -182,7 +182,7 @@ describe ChatMessage::NoteMessage do
it 'returns a message regarding notes on a project snippet' do
expect(subject.pretext).to eq(
- 'test.user [commented on snippet $5](http://url.com) in [project_name](http://somewhere.com): *snippet title*')
+ 'Test User (test.user) [commented on snippet $5](http://url.com) in [project_name](http://somewhere.com): *snippet title*')
expect(subject.attachments).to eq('comment on a snippet')
end
end
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
index 43b02568cb9..0ff20400999 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe ChatMessage::PipelineMessage do
subject { described_class.new(args) }
- let(:user) { { name: 'hacker' } }
+ let(:user) { { name: "The Hacker", username: 'hacker' } }
let(:duration) { 7210 }
let(:args) do
{
@@ -22,12 +22,13 @@ describe ChatMessage::PipelineMessage do
user: user
}
end
+ let(:combined_name) { "The Hacker (hacker)" }
context 'without markdown' do
context 'pipeline succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
- let(:message) { build_message('passed') }
+ let(:message) { build_message('passed', combined_name) }
it 'returns a message with information about succeeded build' do
expect(subject.pretext).to be_empty
@@ -39,7 +40,7 @@ describe ChatMessage::PipelineMessage do
context 'pipeline failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
- let(:message) { build_message }
+ let(:message) { build_message(status, combined_name) }
it 'returns a message with information about failed build' do
expect(subject.pretext).to be_empty
@@ -75,13 +76,13 @@ describe ChatMessage::PipelineMessage do
context 'pipeline succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
- let(:message) { build_markdown_message('passed') }
+ let(:message) { build_markdown_message('passed', combined_name) }
it 'returns a message with information about succeeded build' do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker passed',
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by The Hacker (hacker) passed',
subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 02:00:10',
image: ''
@@ -92,13 +93,13 @@ describe ChatMessage::PipelineMessage do
context 'pipeline failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
- let(:message) { build_markdown_message }
+ let(:message) { build_markdown_message(status, combined_name) }
it 'returns a message with information about failed build' do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker failed',
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by The Hacker (hacker) failed',
subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 02:00:10',
image: ''
diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
index c4adee4f489..7efcba9bcfd 100644
--- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb
+++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
@@ -29,7 +29,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq(
- 'test.user created <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
+ 'Test User (test.user) created <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
'*Wiki page title*')
end
end
@@ -41,7 +41,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq(
- 'test.user edited <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
+ 'Test User (test.user) edited <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
'*Wiki page title*')
end
end
@@ -95,7 +95,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq(
- 'test.user created [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
+ 'Test User (test.user) created [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
end
end
@@ -106,7 +106,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq(
- 'test.user edited [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
+ 'Test User (test.user) edited [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
end
end
end
@@ -141,7 +141,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns the attachment for a new wiki page' do
expect(subject.activity).to eq({
- title: 'test.user created [wiki page](http://url.com)',
+ title: 'Test User (test.user) created [wiki page](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'Wiki page title',
image: 'http://someavatar.com'
@@ -156,7 +156,7 @@ describe ChatMessage::WikiPageMessage do
it 'returns the attachment for an updated wiki page' do
expect(subject.activity).to eq({
- title: 'test.user edited [wiki page](http://url.com)',
+ title: 'Test User (test.user) edited [wiki page](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'Wiki page title',
image: 'http://someavatar.com'
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index dedf3008994..984832b6959 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -57,6 +57,7 @@ describe Project do
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:builds) }
+ it { is_expected.to have_many(:build_trace_section_names)}
it { is_expected.to have_many(:runner_projects) }
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:active_runners) }
@@ -76,6 +77,7 @@ describe Project do
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
+ it { is_expected.to have_one(:cluster) }
context 'after initialized' do
it "has a project_feature" do
@@ -421,20 +423,10 @@ describe Project do
end
describe '#repository_storage_path' do
- let(:project) { create(:project, repository_storage: 'custom') }
-
- before do
- FileUtils.mkdir('tmp/tests/custom_repositories')
- storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- end
-
- after do
- FileUtils.rm_rf('tmp/tests/custom_repositories')
- end
+ let(:project) { create(:project) }
it 'returns the repository storage path' do
- expect(project.repository_storage_path).to eq('tmp/tests/custom_repositories')
+ expect(Dir.exist?(project.repository_storage_path)).to be(true)
end
end
@@ -701,6 +693,44 @@ describe Project do
project.cache_has_external_issue_tracker
end.to change { project.has_external_issue_tracker}.to(false)
end
+
+ it 'does not cache data when in a read-only GitLab instance' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect do
+ project.cache_has_external_issue_tracker
+ end.not_to change { project.has_external_issue_tracker }
+ end
+ end
+
+ describe '#cache_has_external_wiki' do
+ let(:project) { create(:project, has_external_wiki: nil) }
+
+ it 'stores true if there is any external_wikis' do
+ services = double(:service, external_wikis: [ExternalWikiService.new])
+ expect(project).to receive(:services).and_return(services)
+
+ expect do
+ project.cache_has_external_wiki
+ end.to change { project.has_external_wiki}.to(true)
+ end
+
+ it 'stores false if there is no external_wikis' do
+ services = double(:service, external_wikis: [])
+ expect(project).to receive(:services).and_return(services)
+
+ expect do
+ project.cache_has_external_wiki
+ end.to change { project.has_external_wiki}.to(false)
+ end
+
+ it 'does not cache data when in a read-only GitLab instance' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect do
+ project.cache_has_external_wiki
+ end.not_to change { project.has_external_wiki }
+ end
end
describe '#has_wiki?' do
@@ -844,7 +874,7 @@ describe Project do
let(:project) { create(:project) }
context 'when avatar file is uploaded' do
- let(:project) { create(:project, :with_avatar) }
+ let(:project) { create(:project, :public, :with_avatar) }
let(:avatar_path) { "/uploads/-/system/project/avatar/#{project.id}/dk.png" }
let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
@@ -1841,6 +1871,59 @@ describe Project do
end
end
+ context 'forks' do
+ include ProjectForksHelper
+
+ let(:project) { create(:project, :public) }
+ let!(:forked_project) { fork_project(project) }
+
+ describe '#fork_network' do
+ it 'includes a fork of the project' do
+ expect(project.fork_network.projects).to include(forked_project)
+ end
+
+ it 'includes a fork of a fork' do
+ other_fork = fork_project(forked_project)
+
+ expect(project.fork_network.projects).to include(other_fork)
+ end
+
+ it 'includes sibling forks' do
+ other_fork = fork_project(project)
+
+ expect(forked_project.fork_network.projects).to include(other_fork)
+ end
+
+ it 'includes the base project' do
+ expect(forked_project.fork_network.projects).to include(project.reload)
+ end
+ end
+
+ describe '#in_fork_network_of?' do
+ it 'is true for a real fork' do
+ expect(forked_project.in_fork_network_of?(project)).to be_truthy
+ end
+
+ it 'is true for a fork of a fork', :postgresql do
+ other_fork = fork_project(forked_project)
+
+ expect(other_fork.in_fork_network_of?(project)).to be_truthy
+ end
+
+ it 'is true for sibling forks' do
+ sibling = fork_project(project)
+
+ expect(sibling.in_fork_network_of?(forked_project)).to be_truthy
+ end
+
+ it 'is false when another project is given' do
+ other_project = build_stubbed(:project)
+
+ expect(forked_project.in_fork_network_of?(other_project)).to be_falsy
+ end
+ end
+ end
+
describe '#pushes_since_gc' do
let(:project) { create(:project) }
@@ -2477,7 +2560,7 @@ describe Project do
expect(project.migrate_to_hashed_storage!).to be_truthy
end
- it 'flags as readonly' do
+ it 'flags as read-only' do
expect { project.migrate_to_hashed_storage! }.to change { project.repository_read_only }.to(true)
end
@@ -2604,7 +2687,7 @@ describe Project do
expect(project.migrate_to_hashed_storage!).to be_nil
end
- it 'does not flag as readonly' do
+ it 'does not flag as read-only' do
expect { project.migrate_to_hashed_storage! }.not_to change { project.repository_read_only }
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 7156c1b7aa8..5d78aed5b4f 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -40,7 +40,7 @@ describe Repository do
it { is_expected.not_to include('feature') }
it { is_expected.not_to include('fix') }
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.branch_names_contains(sample_commit.id)
@@ -158,7 +158,7 @@ describe Repository do
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore')
@@ -171,7 +171,7 @@ describe Repository do
it_behaves_like 'getting last commit for path'
end
- context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
it_behaves_like 'getting last commit for path'
end
end
@@ -192,7 +192,7 @@ describe Repository do
is_expected.to eq('c1acaa5')
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id
@@ -205,7 +205,7 @@ describe Repository do
it_behaves_like 'getting last commit ID for path'
end
- context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
it_behaves_like 'getting last commit ID for path'
end
end
@@ -255,11 +255,11 @@ describe Repository do
it_behaves_like 'finding commits by message'
end
- context 'when Gitaly commits_by_message feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly commits_by_message feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding commits by message'
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') }
end
@@ -589,7 +589,7 @@ describe Repository do
expect(results).to match_array([])
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.search_files_by_content('feature', 'master')
@@ -626,7 +626,7 @@ describe Repository do
expect(results).to match_array([])
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') }
end
@@ -634,7 +634,11 @@ describe Repository do
end
describe '#fetch_ref' do
- describe 'when storage is broken', broken_storage: true do
+ # Setting the var here, sidesteps the stub that makes gitaly raise an error
+ # before the actual test call
+ set(:broken_repository) { create(:project, :broken_storage).repository }
+
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2')
@@ -844,7 +848,7 @@ describe Repository do
end
end
- context 'with Gitaly disabled', skip_gitaly_mock: true do
+ context 'with Gitaly disabled', :skip_gitaly_mock do
context 'when pre hooks were successful' do
it 'runs without errors' do
hook = double(trigger: [true, nil])
@@ -1097,7 +1101,7 @@ describe Repository do
expect(repository.exists?).to eq(false)
end
- context 'with broken storage', broken_storage: true do
+ context 'with broken storage', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.exists? }
end
@@ -1109,7 +1113,7 @@ describe Repository do
it_behaves_like 'repo exists check'
end
- context 'when repository_exists is enabled', skip_gitaly_mock: true do
+ context 'when repository_exists is enabled', :skip_gitaly_mock do
it_behaves_like 'repo exists check'
end
end
@@ -1672,7 +1676,7 @@ describe Repository do
it_behaves_like 'adding tag'
end
- context 'when Gitaly operation_user_add_tag feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly operation_user_add_tag feature is disabled', :skip_gitaly_mock do
it_behaves_like 'adding tag'
it 'passes commit SHA to pre-receive and update hooks and tag SHA to post-receive hook' do
@@ -1731,7 +1735,7 @@ describe Repository do
end
end
- context 'with gitaly disabled', skip_gitaly_mock: true do
+ context 'with gitaly disabled', :skip_gitaly_mock do
it_behaves_like "user deleting a branch"
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
@@ -1790,7 +1794,7 @@ describe Repository do
it_behaves_like 'removing tag'
end
- context 'when Gitaly operation_user_delete_tag feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do
it_behaves_like 'removing tag'
end
end
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
index 8f05deb8b15..5ec04b99957 100644
--- a/spec/models/sent_notification_spec.rb
+++ b/spec/models/sent_notification_spec.rb
@@ -1,6 +1,9 @@
require 'spec_helper'
describe SentNotification do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+
describe 'validation' do
describe 'note validity' do
context "when the project doesn't match the noteable's project" do
@@ -34,7 +37,6 @@ describe SentNotification do
end
describe '.record' do
- let(:user) { create(:user) }
let(:issue) { create(:issue) }
it 'creates a new SentNotification' do
@@ -43,7 +45,6 @@ describe SentNotification do
end
describe '.record_note' do
- let(:user) { create(:user) }
let(:note) { create(:diff_note_on_merge_request) }
it 'creates a new SentNotification' do
@@ -51,6 +52,123 @@ describe SentNotification do
end
end
+ describe '#unsubscribable?' do
+ shared_examples 'an unsubscribable notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for #{noteable_type}" do
+ it { expect(subject).to be_unsubscribable }
+ end
+ end
+
+ shared_examples 'a non-unsubscribable notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for a #{noteable_type}" do
+ it { expect(subject).not_to be_unsubscribable }
+ end
+ end
+
+ it_behaves_like 'an unsubscribable notification', 'issue' do
+ let(:noteable) { create(:issue, project: project) }
+ end
+
+ it_behaves_like 'an unsubscribable notification', 'merge request' do
+ let(:noteable) { create(:merge_request, source_project: project) }
+ end
+
+ it_behaves_like 'a non-unsubscribable notification', 'commit' do
+ let(:project) { create(:project, :repository) }
+ let(:noteable) { project.commit }
+ end
+
+ it_behaves_like 'a non-unsubscribable notification', 'personal snippet' do
+ let(:noteable) { create(:personal_snippet, project: project) }
+ end
+
+ it_behaves_like 'a non-unsubscribable notification', 'project snippet' do
+ let(:noteable) { create(:project_snippet, project: project) }
+ end
+ end
+
+ describe '#for_commit?' do
+ shared_examples 'a commit notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for #{noteable_type}" do
+ it { expect(subject).to be_for_commit }
+ end
+ end
+
+ shared_examples 'a non-commit notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for a #{noteable_type}" do
+ it { expect(subject).not_to be_for_commit }
+ end
+ end
+
+ it_behaves_like 'a non-commit notification', 'issue' do
+ let(:noteable) { create(:issue, project: project) }
+ end
+
+ it_behaves_like 'a non-commit notification', 'merge request' do
+ let(:noteable) { create(:merge_request, source_project: project) }
+ end
+
+ it_behaves_like 'a commit notification', 'commit' do
+ let(:project) { create(:project, :repository) }
+ let(:noteable) { project.commit }
+ end
+
+ it_behaves_like 'a non-commit notification', 'personal snippet' do
+ let(:noteable) { create(:personal_snippet, project: project) }
+ end
+
+ it_behaves_like 'a non-commit notification', 'project snippet' do
+ let(:noteable) { create(:project_snippet, project: project) }
+ end
+ end
+
+ describe '#for_snippet?' do
+ shared_examples 'a snippet notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for #{noteable_type}" do
+ it { expect(subject).to be_for_snippet }
+ end
+ end
+
+ shared_examples 'a non-snippet notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for a #{noteable_type}" do
+ it { expect(subject).not_to be_for_snippet }
+ end
+ end
+
+ it_behaves_like 'a non-snippet notification', 'issue' do
+ let(:noteable) { create(:issue, project: project) }
+ end
+
+ it_behaves_like 'a non-snippet notification', 'merge request' do
+ let(:noteable) { create(:merge_request, source_project: project) }
+ end
+
+ it_behaves_like 'a non-snippet notification', 'commit' do
+ let(:project) { create(:project, :repository) }
+ let(:noteable) { project.commit }
+ end
+
+ it_behaves_like 'a snippet notification', 'personal snippet' do
+ let(:noteable) { create(:personal_snippet, project: project) }
+ end
+
+ it_behaves_like 'a snippet notification', 'project snippet' do
+ let(:noteable) { create(:project_snippet, project: project) }
+ end
+ end
+
describe '#create_reply' do
context 'for issue' do
let(:issue) { create(:issue) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 62890dd5002..1c3c9068f12 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe User do
include Gitlab::CurrentSettings
+ include ProjectForksHelper
describe 'modules' do
subject { described_class }
@@ -360,9 +361,22 @@ describe User do
expect(external_user.projects_limit).to be 0
end
end
+
+ describe '#check_for_verified_email' do
+ let(:user) { create(:user) }
+ let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) }
+
+ it 'allows a verfied secondary email to be used as the primary without needing reconfirmation' do
+ user.update_attributes!(email: secondary.email)
+ user.reload
+ expect(user.email).to eq secondary.email
+ expect(user.unconfirmed_email).to eq nil
+ expect(user.confirmed?).to be_truthy
+ end
+ end
end
- describe 'after update hook' do
+ describe 'after commit hook' do
describe '.update_invalid_gpg_signatures' do
let(:user) do
create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
@@ -376,10 +390,50 @@ describe User do
end
it 'synchronizes the gpg keys when the email is updated' do
- expect(user).to receive(:update_invalid_gpg_signatures)
+ expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice)
user.update_attributes!(email: 'shawnee.ritchie@denesik.com')
end
end
+
+ describe '#update_emails_with_primary_email' do
+ before do
+ @user = create(:user, email: 'primary@example.com').tap do |user|
+ user.skip_reconfirmation!
+ end
+ @secondary = create :email, email: 'secondary@example.com', user: @user
+ @user.reload
+ end
+
+ it 'gets called when email updated' do
+ expect(@user).to receive(:update_emails_with_primary_email)
+
+ @user.update_attributes!(email: 'new_primary@example.com')
+ end
+
+ it 'adds old primary to secondary emails when secondary is a new email ' do
+ @user.update_attributes!(email: 'new_primary@example.com')
+ @user.reload
+
+ expect(@user.emails.count).to eq 2
+ expect(@user.emails.pluck(:email)).to match_array([@secondary.email, 'primary@example.com'])
+ end
+
+ it 'adds old primary to secondary emails if secondary is becoming a primary' do
+ @user.update_attributes!(email: @secondary.email)
+ @user.reload
+
+ expect(@user.emails.count).to eq 1
+ expect(@user.emails.first.email).to eq 'primary@example.com'
+ end
+
+ it 'transfers old confirmation values into new secondary' do
+ @user.update_attributes!(email: @secondary.email)
+ @user.reload
+
+ expect(@user.emails.count).to eq 1
+ expect(@user.emails.first.confirmed_at).not_to eq nil
+ end
+ end
end
describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do
@@ -467,6 +521,7 @@ describe User do
describe '#generate_password' do
it "does not generate password by default" do
user = create(:user, password: 'abcdefghe')
+
expect(user.password).to eq('abcdefghe')
end
end
@@ -474,6 +529,7 @@ describe User do
describe 'authentication token' do
it "has authentication token" do
user = create(:user)
+
expect(user.authentication_token).not_to be_blank
end
end
@@ -481,6 +537,7 @@ describe User do
describe 'ensure incoming email token' do
it 'has incoming email token' do
user = create(:user)
+
expect(user.incoming_email_token).not_to be_blank
end
end
@@ -523,6 +580,7 @@ describe User do
it 'ensures an rss token on read' do
user = create(:user, rss_token: nil)
rss_token = user.rss_token
+
expect(rss_token).not_to be_blank
expect(user.reload.rss_token).to eq rss_token
end
@@ -633,6 +691,7 @@ describe User do
it "blocks user" do
user.block
+
expect(user.blocked?).to be_truthy
end
end
@@ -966,6 +1025,7 @@ describe User do
it 'is case-insensitive' do
user = create(:user, username: 'JohnDoe')
+
expect(described_class.find_by_username('JOHNDOE')).to eq user
end
end
@@ -978,6 +1038,7 @@ describe User do
it 'is case-insensitive' do
user = create(:user, username: 'JohnDoe')
+
expect(described_class.find_by_username!('JOHNDOE')).to eq user
end
end
@@ -1067,11 +1128,13 @@ describe User do
it 'is true if avatar is image' do
user.update_attribute(:avatar, 'uploads/avatar.png')
+
expect(user.avatar_type).to be_truthy
end
it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
+
expect(user.avatar_type).to eq(['only images allowed'])
end
end
@@ -1094,6 +1157,50 @@ describe User do
end
end
+ describe '#all_emails' do
+ let(:user) { create(:user) }
+
+ it 'returns all emails' do
+ email_confirmed = create :email, user: user, confirmed_at: Time.now
+ email_unconfirmed = create :email, user: user
+ user.reload
+
+ expect(user.all_emails).to match_array([user.email, email_unconfirmed.email, email_confirmed.email])
+ end
+ end
+
+ describe '#verified_emails' do
+ let(:user) { create(:user) }
+
+ it 'returns only confirmed emails' do
+ email_confirmed = create :email, user: user, confirmed_at: Time.now
+ create :email, user: user
+ user.reload
+
+ expect(user.verified_emails).to match_array([user.email, email_confirmed.email])
+ end
+ end
+
+ describe '#verified_email?' do
+ let(:user) { create(:user) }
+
+ it 'returns true when the email is verified/confirmed' do
+ email_confirmed = create :email, user: user, confirmed_at: Time.now
+ create :email, user: user
+ user.reload
+
+ expect(user.verified_email?(user.email)).to be_truthy
+ expect(user.verified_email?(email_confirmed.email.titlecase)).to be_truthy
+ end
+
+ it 'returns false when the email is not verified/confirmed' do
+ email_unconfirmed = create :email, user: user
+ user.reload
+
+ expect(user.verified_email?(email_unconfirmed.email)).to be_falsy
+ end
+ end
+
describe '#requires_ldap_check?' do
let(:user) { described_class.new }
@@ -1101,6 +1208,7 @@ describe User do
# Create a condition which would otherwise cause 'true' to be returned
allow(user).to receive(:ldap_user?).and_return(true)
user.last_credential_check_at = nil
+
expect(user.requires_ldap_check?).to be_falsey
end
@@ -1111,6 +1219,7 @@ describe User do
it 'is false for non-LDAP users' do
allow(user).to receive(:ldap_user?).and_return(false)
+
expect(user.requires_ldap_check?).to be_falsey
end
@@ -1121,11 +1230,13 @@ describe User do
it 'is true when the user has never had an LDAP check before' do
user.last_credential_check_at = nil
+
expect(user.requires_ldap_check?).to be_truthy
end
it 'is true when the last LDAP check happened over 1 hour ago' do
user.last_credential_check_at = 2.hours.ago
+
expect(user.requires_ldap_check?).to be_truthy
end
end
@@ -1136,16 +1247,19 @@ describe User do
describe '#ldap_user?' do
it 'is true if provider name starts with ldap' do
user = create(:omniauth_user, provider: 'ldapmain')
+
expect(user.ldap_user?).to be_truthy
end
it 'is false for other providers' do
user = create(:omniauth_user, provider: 'other-provider')
+
expect(user.ldap_user?).to be_falsey
end
it 'is false if no extern_uid is provided' do
user = create(:omniauth_user, extern_uid: nil)
+
expect(user.ldap_user?).to be_falsey
end
end
@@ -1153,6 +1267,7 @@ describe User do
describe '#ldap_identity' do
it 'returns ldap identity' do
user = create :omniauth_user
+
expect(user.ldap_identity.provider).not_to be_empty
end
end
@@ -1162,6 +1277,7 @@ describe User do
it 'blocks user flaging the action caming from ldap' do
user.ldap_block
+
expect(user.blocked?).to be_truthy
expect(user.ldap_blocked?).to be_truthy
end
@@ -1234,18 +1350,22 @@ describe User do
expect(user.starred?(project2)).to be_falsey
star1 = UsersStarProject.create!(project: project1, user: user)
+
expect(user.starred?(project1)).to be_truthy
expect(user.starred?(project2)).to be_falsey
star2 = UsersStarProject.create!(project: project2, user: user)
+
expect(user.starred?(project1)).to be_truthy
expect(user.starred?(project2)).to be_truthy
star1.destroy
+
expect(user.starred?(project1)).to be_falsey
expect(user.starred?(project2)).to be_truthy
star2.destroy
+
expect(user.starred?(project1)).to be_falsey
expect(user.starred?(project2)).to be_falsey
end
@@ -1257,9 +1377,13 @@ describe User do
project = create(:project, :public)
expect(user.starred?(project)).to be_falsey
+
user.toggle_star(project)
+
expect(user.starred?(project)).to be_truthy
+
user.toggle_star(project)
+
expect(user.starred?(project)).to be_falsey
end
end
@@ -1308,7 +1432,7 @@ describe User do
describe "#contributed_projects" do
subject { create(:user) }
let!(:project1) { create(:project) }
- let!(:project2) { create(:project, forked_from_project: project3) }
+ let!(:project2) { fork_project(project3) }
let!(:project3) { create(:project) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
let!(:push_event) { create(:push_event, project: project1, author: subject) }
@@ -1332,6 +1456,23 @@ describe User do
end
end
+ describe '#fork_of' do
+ let(:user) { create(:user) }
+
+ it "returns a user's fork of a project" do
+ project = create(:project, :public)
+ user_fork = fork_project(project, user, namespace: user.namespace)
+
+ expect(user.fork_of(project)).to eq(user_fork)
+ end
+
+ it 'returns nil if the project does not have a fork network' do
+ project = create(:project)
+
+ expect(user.fork_of(project)).to be_nil
+ end
+ end
+
describe '#can_be_removed?' do
subject { create(:user) }
@@ -1384,7 +1525,7 @@ describe User do
it { is_expected.to eq([private_group]) }
end
- describe '#authorized_projects', truncate: true do
+ describe '#authorized_projects', :truncate do
context 'with a minimum access level' do
it 'includes projects for which the user is an owner' do
user = create(:user)
@@ -1438,9 +1579,11 @@ describe User do
user = create(:user)
member = group.add_developer(user)
+
expect(user.authorized_projects).to include(project)
member.destroy
+
expect(user.authorized_projects).not_to include(project)
end
@@ -1461,9 +1604,11 @@ describe User do
project = create(:project, :private, namespace: user1.namespace)
project.team << [user2, Gitlab::Access::DEVELOPER]
+
expect(user2.authorized_projects).to include(project)
project.destroy
+
expect(user2.authorized_projects).not_to include(project)
end
@@ -1473,9 +1618,11 @@ describe User do
user = create(:user)
group.add_developer(user)
+
expect(user.authorized_projects).to include(project)
group.destroy
+
expect(user.authorized_projects).not_to include(project)
end
end
@@ -1730,7 +1877,7 @@ describe User do
end
end
- describe '#refresh_authorized_projects', clean_gitlab_redis_shared_state: true do
+ describe '#refresh_authorized_projects', :clean_gitlab_redis_shared_state do
let(:project1) { create(:project) }
let(:project2) { create(:project) }
let(:user) { create(:user) }
@@ -2019,7 +2166,9 @@ describe User do
it 'creates the namespace' do
expect(user.namespace).to be_nil
+
user.save!
+
expect(user.namespace).not_to be_nil
end
end
@@ -2040,11 +2189,13 @@ describe User do
it 'updates the namespace name' do
user.update_attributes!(username: new_username)
+
expect(user.namespace.name).to eq(new_username)
end
it 'updates the namespace path' do
user.update_attributes!(username: new_username)
+
expect(user.namespace.path).to eq(new_username)
end
@@ -2058,6 +2209,7 @@ describe User do
it 'adds the namespace errors to the user' do
user.update_attributes(username: new_username)
+
expect(user.errors.full_messages.first).to eq('Namespace name has already been taken')
end
end
@@ -2074,56 +2226,49 @@ describe User do
end
end
- describe '#verified_email?' do
- it 'returns true when the email is the primary email' do
- user = build :user, email: 'email@example.com'
-
- expect(user.verified_email?('email@example.com')).to be true
- end
-
- it 'returns false when the email is not the primary email' do
- user = build :user, email: 'email@example.com'
-
- expect(user.verified_email?('other_email@example.com')).to be false
- end
- end
-
describe '#sync_attribute?' do
let(:user) { described_class.new }
context 'oauth user' do
it 'returns true if name can be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(name location))
+
expect(user.sync_attribute?(:name)).to be_truthy
end
it 'returns true if email can be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(name email))
+
expect(user.sync_attribute?(:email)).to be_truthy
end
it 'returns true if location can be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(location email))
+
expect(user.sync_attribute?(:email)).to be_truthy
end
it 'returns false if name can not be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(location email))
+
expect(user.sync_attribute?(:name)).to be_falsey
end
it 'returns false if email can not be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(location email))
+
expect(user.sync_attribute?(:name)).to be_falsey
end
it 'returns false if location can not be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(location email))
+
expect(user.sync_attribute?(:name)).to be_falsey
end
it 'returns true for all syncable attributes if all syncable attributes can be synced' do
stub_omniauth_setting(sync_profile_attributes: true)
+
expect(user.sync_attribute?(:name)).to be_truthy
expect(user.sync_attribute?(:email)).to be_truthy
expect(user.sync_attribute?(:location)).to be_truthy
@@ -2139,6 +2284,7 @@ describe User do
context 'ldap user' do
it 'returns true for email if ldap user' do
allow(user).to receive(:ldap_user?).and_return(true)
+
expect(user.sync_attribute?(:name)).to be_falsey
expect(user.sync_attribute?(:email)).to be_truthy
expect(user.sync_attribute?(:location)).to be_falsey
@@ -2147,10 +2293,56 @@ describe User do
it 'returns true for email and location if ldap user and location declared as syncable' do
allow(user).to receive(:ldap_user?).and_return(true)
stub_omniauth_setting(sync_profile_attributes: %w(location))
+
expect(user.sync_attribute?(:name)).to be_falsey
expect(user.sync_attribute?(:email)).to be_truthy
expect(user.sync_attribute?(:location)).to be_truthy
end
end
end
+
+ describe '#confirm_deletion_with_password?' do
+ where(
+ password_automatically_set: [true, false],
+ ldap_user: [true, false],
+ password_authentication_disabled: [true, false]
+ )
+
+ with_them do
+ let!(:user) { create(:user, password_automatically_set: password_automatically_set) }
+ let!(:identity) { create(:identity, user: user) if ldap_user }
+
+ # Only confirm deletion with password if all inputs are false
+ let(:expected) { !(password_automatically_set || ldap_user || password_authentication_disabled) }
+
+ before do
+ stub_application_setting(password_authentication_enabled: !password_authentication_disabled)
+ end
+
+ it 'returns false unless all inputs are true' do
+ expect(user.confirm_deletion_with_password?).to eq(expected)
+ end
+ end
+ end
+
+ describe '#delete_async' do
+ let(:user) { create(:user) }
+ let(:deleted_by) { create(:user) }
+
+ it 'blocks the user then schedules them for deletion if a hard delete is specified' do
+ expect(DeleteUserWorker).to receive(:perform_async).with(deleted_by.id, user.id, hard_delete: true)
+
+ user.delete_async(deleted_by: deleted_by, params: { hard_delete: true })
+
+ expect(user).to be_blocked
+ end
+
+ it 'schedules user for deletion without blocking them' do
+ expect(DeleteUserWorker).to receive(:perform_async).with(deleted_by.id, user.id, {})
+
+ user.delete_async(deleted_by: deleted_by)
+
+ expect(user).not_to be_blocked
+ end
+ end
end
diff --git a/spec/policies/gcp/cluster_policy_spec.rb b/spec/policies/gcp/cluster_policy_spec.rb
new file mode 100644
index 00000000000..e213aa3d557
--- /dev/null
+++ b/spec/policies/gcp/cluster_policy_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gcp::ClusterPolicy, :models do
+ set(:project) { create(:project) }
+ set(:cluster) { create(:gcp_cluster, project: project) }
+ let(:user) { create(:user) }
+ let(:policy) { described_class.new(user, cluster) }
+
+ describe 'rules' do
+ context 'when developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when master' do
+ before do
+ project.add_master(user)
+ end
+
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+ end
+end
diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb
new file mode 100644
index 00000000000..2cf669e8191
--- /dev/null
+++ b/spec/policies/issuable_policy_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe IssuablePolicy, models: true do
+ describe '#rules' do
+ context 'when discussion is locked for the issuable' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project, discussion_locked: true) }
+ let(:policies) { described_class.new(user, issue) }
+
+ context 'when the user is not a project member' do
+ it 'can not create a note' do
+ expect(policies).to be_disallowed(:create_note)
+ end
+ end
+
+ context 'when the user is a project member' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'can create a note' do
+ expect(policies).to be_allowed(:create_note)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
new file mode 100644
index 00000000000..58d36a2c84e
--- /dev/null
+++ b/spec/policies/note_policy_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe NotePolicy, mdoels: true do
+ describe '#rules' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ def policies(noteable = nil)
+ return @policies if @policies
+
+ noteable ||= issue
+ note = create(:note, noteable: noteable, author: user, project: project)
+
+ @policies = described_class.new(user, note)
+ end
+
+ context 'when the project is public' do
+ context 'when the note author is not a project member' do
+ it 'can edit a note' do
+ expect(policies).to be_allowed(:update_note)
+ expect(policies).to be_allowed(:admin_note)
+ expect(policies).to be_allowed(:resolve_note)
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+
+ context 'when the noteable is a snippet' do
+ it 'can edit note' do
+ policies = policies(create(:project_snippet, project: project))
+
+ expect(policies).to be_allowed(:update_note)
+ expect(policies).to be_allowed(:admin_note)
+ expect(policies).to be_allowed(:resolve_note)
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+
+ context 'when a discussion is locked' do
+ before do
+ issue.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when the note author is a project member' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'can edit a note' do
+ expect(policies).to be_allowed(:update_note)
+ expect(policies).to be_allowed(:admin_note)
+ expect(policies).to be_allowed(:resolve_note)
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+
+ context 'when the note author is not a project member' do
+ it 'can not edit a note' do
+ expect(policies).to be_disallowed(:update_note)
+ expect(policies).to be_disallowed(:admin_note)
+ expect(policies).to be_disallowed(:resolve_note)
+ end
+
+ it 'can read a note' do
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb
index e4886a8f019..f7ceaf844be 100644
--- a/spec/presenters/ci/pipeline_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_presenter_spec.rb
@@ -51,4 +51,21 @@ describe Ci::PipelinePresenter do
end
end
end
+
+ context '#failure_reason' do
+ context 'when pipeline has failure reason' do
+ it 'represents a failure reason sentence' do
+ pipeline.failure_reason = :config_error
+
+ expect(presenter.failure_reason)
+ .to eq 'CI/CD YAML configuration error!'
+ end
+ end
+
+ context 'when pipeline does not have failure reason' do
+ it 'returns nil' do
+ expect(presenter.failure_reason).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/presenters/gcp/cluster_presenter_spec.rb b/spec/presenters/gcp/cluster_presenter_spec.rb
new file mode 100644
index 00000000000..8d86dc31582
--- /dev/null
+++ b/spec/presenters/gcp/cluster_presenter_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gcp::ClusterPresenter do
+ let(:project) { create(:project) }
+ let(:cluster) { create(:gcp_cluster, project: project) }
+
+ subject(:presenter) do
+ described_class.new(cluster)
+ end
+
+ it 'inherits from Gitlab::View::Presenter::Delegated' do
+ expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ end
+
+ describe '#initialize' do
+ it 'takes a cluster and optional params' do
+ expect { presenter }.not_to raise_error
+ end
+
+ it 'exposes cluster' do
+ expect(presenter.cluster).to eq(cluster)
+ end
+
+ it 'forwards missing methods to cluster' do
+ expect(presenter.gcp_cluster_zone).to eq(cluster.gcp_cluster_zone)
+ end
+ end
+
+ describe '#gke_cluster_url' do
+ subject { described_class.new(cluster).gke_cluster_url }
+
+ it { is_expected.to include(cluster.gcp_cluster_zone) }
+ it { is_expected.to include(cluster.gcp_cluster_name) }
+ end
+end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 060c8902471..862920ad7c3 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -1,4 +1,6 @@
require 'spec_helper'
+require 'raven/transports/dummy'
+require_relative '../../../config/initializers/sentry'
describe API::Helpers do
include API::APIGuard::HelperMethods
@@ -476,7 +478,7 @@ describe API::Helpers do
allow(exception).to receive(:backtrace).and_return(caller)
expect_any_instance_of(self.class).to receive(:sentry_context)
- expect(Raven).to receive(:capture_exception).with(exception)
+ expect(Raven).to receive(:capture_exception).with(exception, extra: {})
handle_api_exception(exception)
end
@@ -501,6 +503,30 @@ describe API::Helpers do
expect(json_response['message']).to start_with("\nRuntimeError (Runtime Error!):")
end
end
+
+ context 'extra information' do
+ # Sentry events are an array of the form [auth_header, data, options]
+ let(:event_data) { Raven.client.transport.events.first[1] }
+
+ before do
+ stub_application_setting(
+ sentry_enabled: true,
+ sentry_dsn: "dummy://12345:67890@sentry.localdomain/sentry/42"
+ )
+ configure_sentry
+ Raven.client.configuration.encoding = 'json'
+ end
+
+ it 'sends the params, excluding confidential values' do
+ expect(Gitlab::Sentry).to receive(:enabled?).twice.and_return(true)
+ expect(ProjectsFinder).to receive(:new).and_raise('Runtime Error!')
+
+ get api('/projects', user), password: 'dont_send_this', other_param: 'send_this'
+
+ expect(event_data).to include('other_param=send_this')
+ expect(event_data).to include('password=********')
+ end
+ end
end
describe '.authenticate_non_get!' do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index c4f6e97b915..5e66e1607ba 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1,6 +1,8 @@
require "spec_helper"
describe API::MergeRequests do
+ include ProjectForksHelper
+
let(:base_time) { Time.now }
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
@@ -616,17 +618,17 @@ describe API::MergeRequests do
context 'forked projects' do
let!(:user2) { create(:user) }
- let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
+ let!(:forked_project) { fork_project(project, user2) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before do
- fork_project.add_reporter(user2)
+ forked_project.add_reporter(user2)
allow_any_instance_of(MergeRequest).to receive(:write_ref)
end
it "returns merge_request" do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
expect(response).to have_gitlab_http_status(201)
@@ -635,10 +637,10 @@ describe API::MergeRequests do
end
it "does not return 422 when source_branch equals target_branch" do
- expect(project.id).not_to eq(fork_project.id)
- expect(fork_project.forked?).to be_truthy
- expect(fork_project.forked_from_project).to eq(project)
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ expect(project.id).not_to eq(forked_project.id)
+ expect(forked_project.forked?).to be_truthy
+ expect(forked_project.forked_from_project).to eq(project)
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
@@ -647,7 +649,7 @@ describe API::MergeRequests do
it 'returns 422 when target project has disabled merge requests' do
project.project_feature.update(merge_requests_access_level: 0)
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test',
target_branch: 'master',
source_branch: 'markdown',
@@ -658,36 +660,26 @@ describe API::MergeRequests do
end
it "returns 400 when source_branch is missing" do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when target_branch is missing" do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when title is missing" do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
context 'when target_branch is specified' do
- it 'returns 422 if not a forked project' do
- post api("/projects/#{project.id}/merge_requests", user),
- title: 'Test merge_request',
- target_branch: 'master',
- source_branch: 'markdown',
- author: user,
- target_project_id: fork_project.id
- expect(response).to have_gitlab_http_status(422)
- end
-
it 'returns 422 if targeting a different fork' do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request',
target_branch: 'master',
source_branch: 'markdown',
@@ -698,8 +690,8 @@ describe API::MergeRequests do
end
it "returns 201 when target_branch is specified and for the same project" do
- post api("/projects/#{fork_project.id}/merge_requests", user2),
- title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
+ post api("/projects/#{forked_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id
expect(response).to have_gitlab_http_status(201)
end
end
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index f5882c0c74a..fb440fa551c 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -302,6 +302,40 @@ describe API::Notes do
expect(private_issue.notes.reload).to be_empty
end
end
+
+ context 'when the merge request discussion is locked' do
+ before do
+ merge_request.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when a user is a team member' do
+ subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user), body: 'Hi!' }
+
+ it 'returns 200 status' do
+ subject
+
+ expect(response).to have_http_status(201)
+ end
+
+ it 'creates a new note' do
+ expect { subject }.to change { Note.count }.by(1)
+ end
+ end
+
+ context 'when a user is not a team member' do
+ subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", private_user), body: 'Hi!' }
+
+ it 'returns 403 status' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'does not create a new note' do
+ expect { subject }.not_to change { Note.count }
+ end
+ end
+ end
end
describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 18f6f7df1fa..5964244f8c5 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -64,9 +64,12 @@ describe API::Projects do
create(:project, :public)
end
+ # TODO: We're currently querying to detect if a project is a fork
+ # in 2 ways. Lower this back to 8 when `ForkedProjectLink` relation is
+ # removed
expect do
get api('/projects', current_user)
- end.not_to exceed_query_limit(control).with_threshold(8)
+ end.not_to exceed_query_limit(control).with_threshold(9)
end
end
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index 86f38dd4ec1..df73c731c96 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -1,6 +1,8 @@
require "spec_helper"
describe API::MergeRequests do
+ include ProjectForksHelper
+
let(:base_time) { Time.now }
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
@@ -312,17 +314,17 @@ describe API::MergeRequests do
context 'forked projects' do
let!(:user2) { create(:user) }
- let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
+ let!(:forked_project) { fork_project(project, user2) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before do
- fork_project.add_reporter(user2)
+ forked_project.add_reporter(user2)
allow_any_instance_of(MergeRequest).to receive(:write_ref)
end
it "returns merge_request" do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
expect(response).to have_gitlab_http_status(201)
@@ -331,10 +333,10 @@ describe API::MergeRequests do
end
it "does not return 422 when source_branch equals target_branch" do
- expect(project.id).not_to eq(fork_project.id)
- expect(fork_project.forked?).to be_truthy
- expect(fork_project.forked_from_project).to eq(project)
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ expect(project.id).not_to eq(forked_project.id)
+ expect(forked_project.forked?).to be_truthy
+ expect(forked_project.forked_from_project).to eq(project)
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
@@ -343,7 +345,7 @@ describe API::MergeRequests do
it "returns 422 when target project has disabled merge requests" do
project.project_feature.update(merge_requests_access_level: 0)
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test',
target_branch: "master",
source_branch: 'markdown',
@@ -354,36 +356,26 @@ describe API::MergeRequests do
end
it "returns 400 when source_branch is missing" do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when target_branch is missing" do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when title is missing" do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
expect(response).to have_gitlab_http_status(400)
end
context 'when target_branch is specified' do
- it 'returns 422 if not a forked project' do
- post v3_api("/projects/#{project.id}/merge_requests", user),
- title: 'Test merge_request',
- target_branch: 'master',
- source_branch: 'markdown',
- author: user,
- target_project_id: fork_project.id
- expect(response).to have_gitlab_http_status(422)
- end
-
it 'returns 422 if targeting a different fork' do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
title: 'Test merge_request',
target_branch: 'master',
source_branch: 'markdown',
@@ -394,8 +386,8 @@ describe API::MergeRequests do
end
it "returns 201 when target_branch is specified and for the same project" do
- post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
- title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
+ post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id
expect(response).to have_gitlab_http_status(201)
end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 27d09b8202e..bca5bf81c5c 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Git LFS API and storage' do
include WorkhorseHelpers
+ include ProjectForksHelper
let(:user) { create(:user) }
let!(:lfs_object) { create(:lfs_object, :with_file) }
@@ -824,6 +825,34 @@ describe 'Git LFS API and storage' do
end
end
+ describe 'when handling lfs batch request on a read-only GitLab instance' do
+ let(:authorization) { authorize_user }
+ let(:project) { create(:project) }
+ let(:path) { "#{project.http_url_to_repo}/info/lfs/objects/batch" }
+ let(:body) do
+ { 'objects' => [{ 'oid' => sample_oid, 'size' => sample_size }] }
+ end
+
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ project.team << [user, :master]
+ enable_lfs
+ end
+
+ it 'responds with a 200 message on download' do
+ post_lfs_json path, body.merge('operation' => 'download'), headers
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'responds with a 403 message on upload' do
+ post_lfs_json path, body.merge('operation' => 'upload'), headers
+
+ expect(response).to have_gitlab_http_status(403)
+ expect(json_response).to include('message' => 'You cannot write to this read-only GitLab instance.')
+ end
+ end
+
describe 'when pushing a lfs object' do
before do
enable_lfs
@@ -1173,11 +1202,6 @@ describe 'Git LFS API and storage' do
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token)
end
- def fork_project(project, user, object = nil)
- allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
- Projects::ForkService.new(project, user, {}).execute
- end
-
def post_lfs_json(url, body = nil, headers = nil)
post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json'))
end
diff --git a/spec/rubocop/cop/migration/datetime_spec.rb b/spec/rubocop/cop/migration/datetime_spec.rb
index 388b086ce6a..b1dfcf1b048 100644
--- a/spec/rubocop/cop/migration/datetime_spec.rb
+++ b/spec/rubocop/cop/migration/datetime_spec.rb
@@ -9,6 +9,7 @@ describe RuboCop::Cop::Migration::Datetime do
include CopHelper
subject(:cop) { described_class.new }
+
let(:migration_with_datetime) do
%q(
class Users < ActiveRecord::Migration
@@ -22,6 +23,19 @@ describe RuboCop::Cop::Migration::Datetime do
)
end
+ let(:migration_with_timestamp) do
+ %q(
+ class Users < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column(:users, :username, :text)
+ add_column(:users, :last_sign_in, :timestamp)
+ end
+ end
+ )
+ end
+
let(:migration_without_datetime) do
%q(
class Users < ActiveRecord::Migration
@@ -58,6 +72,17 @@ describe RuboCop::Cop::Migration::Datetime do
aggregate_failures do
expect(cop.offenses.size).to eq(1)
expect(cop.offenses.map(&:line)).to eq([7])
+ expect(cop.offenses.first.message).to include('datetime')
+ end
+ end
+
+ it 'registers an offense when the ":timestamp" data type is used' do
+ inspect_source(cop, migration_with_timestamp)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([7])
+ expect(cop.offenses.first.message).to include('timestamp')
end
end
@@ -81,6 +106,7 @@ describe RuboCop::Cop::Migration::Datetime do
context 'outside of migration' do
it 'registers no offense' do
inspect_source(cop, migration_with_datetime)
+ inspect_source(cop, migration_with_timestamp)
inspect_source(cop, migration_without_datetime)
inspect_source(cop, migration_with_datetime_with_timezone)
diff --git a/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb b/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb
new file mode 100644
index 00000000000..278662d32ea
--- /dev/null
+++ b/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/rspec/verbose_include_metadata'
+
+describe RuboCop::Cop::RSpec::VerboseIncludeMetadata do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ let(:source_file) { 'foo_spec.rb' }
+
+ # Override `CopHelper#inspect_source` to always appear to be in a spec file,
+ # so that our RSpec-only cop actually runs
+ def inspect_source(*args)
+ super(*args, source_file)
+ end
+
+ shared_examples 'examples with include syntax' do |title|
+ it "flags violation for #{title} examples that uses verbose include syntax" do
+ inspect_source(cop, "#{title} 'Test', js: true do; end")
+
+ expect(cop.offenses.size).to eq(1)
+ offense = cop.offenses.first
+
+ expect(offense.line).to eq(1)
+ expect(cop.highlights).to eq(["#{title} 'Test', js: true"])
+ expect(offense.message).to eq('Use `:js` instead of `js: true`.')
+ end
+
+ it "doesn't flag violation for #{title} examples that uses compact include syntax", :aggregate_failures do
+ inspect_source(cop, "#{title} 'Test', :js do; end")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} examples that uses flag: symbol" do
+ inspect_source(cop, "#{title} 'Test', flag: :symbol do; end")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "autocorrects #{title} examples that uses verbose syntax into compact syntax" do
+ autocorrected = autocorrect_source(cop, "#{title} 'Test', js: true do; end", source_file)
+
+ expect(autocorrected).to eql("#{title} 'Test', :js do; end")
+ end
+ end
+
+ %w(describe context feature example_group it specify example scenario its).each do |example|
+ it_behaves_like 'examples with include syntax', example
+ end
+end
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index 5b7822d5d8e..f6bd6e9ede4 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe BuildDetailsEntity do
+ include ProjectForksHelper
+
set(:user) { create(:admin) }
it 'inherits from JobEntity' do
@@ -56,18 +58,16 @@ describe BuildDetailsEntity do
end
context 'when merge request is from a fork' do
- let(:fork_project) do
- create(:project, forked_from_project: project)
- end
+ let(:forked_project) { fork_project(project) }
- let(:pipeline) { create(:ci_pipeline, project: fork_project) }
+ let(:pipeline) { create(:ci_pipeline, project: forked_project) }
before do
allow(build).to receive(:merge_request).and_return(merge_request)
end
let(:merge_request) do
- create(:merge_request, source_project: fork_project,
+ create(:merge_request, source_project: forked_project,
target_project: project,
source_branch: build.ref)
end
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
new file mode 100644
index 00000000000..2c7f49974f1
--- /dev/null
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe ClusterEntity do
+ set(:cluster) { create(:gcp_cluster, :errored) }
+ let(:request) { double('request') }
+
+ let(:entity) do
+ described_class.new(cluster)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains status' do
+ expect(subject[:status]).to eq(:errored)
+ end
+
+ it 'contains status reason' do
+ expect(subject[:status_reason]).to eq('general error')
+ end
+ end
+end
diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb
new file mode 100644
index 00000000000..1ac6784d28f
--- /dev/null
+++ b/spec/serializers/cluster_serializer_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe ClusterSerializer do
+ let(:serializer) do
+ described_class.new
+ end
+
+ describe '#represent_status' do
+ subject { serializer.represent_status(resource) }
+
+ context 'when represents only status' do
+ let(:resource) { create(:gcp_cluster, :errored) }
+
+ it 'serializes only status' do
+ expect(subject.keys).to contain_exactly(:status, :status_reason)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
index 4aeb593da44..87832b3dca1 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe MergeRequestEntity do
- let(:project) { create :project }
+ let(:project) { create :project, :repository }
let(:resource) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index f8df461bc81..248552d1858 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -108,5 +108,18 @@ describe PipelineEntity do
expect(subject[:ref][:path]).to be_nil
end
end
+
+ context 'when pipeline has a failure reason set' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ before do
+ pipeline.drop!(:config_error)
+ end
+
+ it 'has a correct failure reason' do
+ expect(subject[:failure_reason])
+ .to eq 'CI/CD YAML configuration error!'
+ end
+ end
end
end
diff --git a/spec/services/ci/create_cluster_service_spec.rb b/spec/services/ci/create_cluster_service_spec.rb
new file mode 100644
index 00000000000..6e7398fbffa
--- /dev/null
+++ b/spec/services/ci/create_cluster_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Ci::CreateClusterService do
+ describe '#execute' do
+ let(:access_token) { 'xxx' }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:result) { described_class.new(project, user, params).execute(access_token) }
+
+ context 'when correct params' do
+ let(:params) do
+ {
+ gcp_project_id: 'gcp-project',
+ gcp_cluster_name: 'test-cluster',
+ gcp_cluster_zone: 'us-central1-a',
+ gcp_cluster_size: 1
+ }
+ end
+
+ it 'creates a cluster object' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { result }.to change { Gcp::Cluster.count }.by(1)
+ expect(result.gcp_project_id).to eq('gcp-project')
+ expect(result.gcp_cluster_name).to eq('test-cluster')
+ expect(result.gcp_cluster_zone).to eq('us-central1-a')
+ expect(result.gcp_cluster_size).to eq(1)
+ expect(result.gcp_token).to eq(access_token)
+ end
+ end
+
+ context 'when invalid params' do
+ let(:params) do
+ {
+ gcp_project_id: 'gcp-project',
+ gcp_cluster_name: 'test-cluster',
+ gcp_cluster_zone: 'us-central1-a',
+ gcp_cluster_size: 'ABC'
+ }
+ end
+
+ it 'returns an error' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { result }.to change { Gcp::Cluster.count }.by(0)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index eb6e683cc23..08847183bf4 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Ci::CreatePipelineService do
+ include ProjectForksHelper
+
set(:project) { create(:project, :repository) }
let(:user) { create(:admin) }
let(:ref_name) { 'refs/heads/master' }
@@ -82,13 +84,9 @@ describe Ci::CreatePipelineService do
end
context 'when merge request target project is different from source project' do
+ let!(:project) { fork_project(target_project, nil, repository: true) }
let!(:target_project) { create(:project, :repository) }
- let!(:forked_project_link) do
- create(:forked_project_link, forked_to_project: project,
- forked_from_project: target_project)
- end
-
it 'updates head pipeline for merge request' do
merge_request = create(:merge_request, source_branch: 'master',
target_branch: "branch_1",
diff --git a/spec/services/ci/extract_sections_from_build_trace_service_spec.rb b/spec/services/ci/extract_sections_from_build_trace_service_spec.rb
new file mode 100644
index 00000000000..28f2fa7903a
--- /dev/null
+++ b/spec/services/ci/extract_sections_from_build_trace_service_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Ci::ExtractSectionsFromBuildTraceService, '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:build) { create(:ci_build, project: project) }
+
+ subject { described_class.new(project, user) }
+
+ shared_examples 'build trace has sections markers' do
+ before do
+ build.trace.set(File.read(expand_fixture_path('trace/trace_with_sections')))
+ end
+
+ it 'saves the correct extracted sections' do
+ expect(build.trace_sections).to be_empty
+ expect(subject.execute(build)).to be(true)
+ expect(build.trace_sections).not_to be_empty
+ end
+
+ it "fails if trace_sections isn't empty" do
+ expect(subject.execute(build)).to be(true)
+ expect(build.trace_sections).not_to be_empty
+
+ expect(subject.execute(build)).to be(false)
+ expect(build.trace_sections).not_to be_empty
+ end
+ end
+
+ shared_examples 'build trace has no sections markers' do
+ before do
+ build.trace.set('no markerts')
+ end
+
+ it 'extracts no sections' do
+ expect(build.trace_sections).to be_empty
+ expect(subject.execute(build)).to be(true)
+ expect(build.trace_sections).to be_empty
+ end
+ end
+
+ context 'when the build has no user' do
+ it_behaves_like 'build trace has sections markers'
+ it_behaves_like 'build trace has no sections markers'
+ end
+
+ context 'when the build has a valid user' do
+ before do
+ build.user = user
+ end
+
+ it_behaves_like 'build trace has sections markers'
+ it_behaves_like 'build trace has no sections markers'
+ end
+end
diff --git a/spec/services/ci/fetch_gcp_operation_service_spec.rb b/spec/services/ci/fetch_gcp_operation_service_spec.rb
new file mode 100644
index 00000000000..7792979c5cb
--- /dev/null
+++ b/spec/services/ci/fetch_gcp_operation_service_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+require 'google/apis'
+
+describe Ci::FetchGcpOperationService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:operation) { double }
+
+ context 'when suceeded' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_operations).and_return(operation)
+ end
+
+ it 'fetch the gcp operaion' do
+ expect { |b| described_class.new.execute(cluster, &b) }
+ .to yield_with_args(operation)
+ end
+ end
+
+ context 'when raises an error' do
+ let(:error) { Google::Apis::ServerError.new('a') }
+
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_operations).and_raise(error)
+ end
+
+ it 'sets an error to cluster object' do
+ expect { |b| described_class.new.execute(cluster, &b) }
+ .not_to yield_with_args
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/fetch_kubernetes_token_service_spec.rb b/spec/services/ci/fetch_kubernetes_token_service_spec.rb
new file mode 100644
index 00000000000..1d05c9671a9
--- /dev/null
+++ b/spec/services/ci/fetch_kubernetes_token_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Ci::FetchKubernetesTokenService do
+ describe '#execute' do
+ subject { described_class.new(api_url, ca_pem, username, password).execute }
+
+ let(:api_url) { 'http://111.111.111.111' }
+ let(:ca_pem) { '' }
+ let(:username) { 'admin' }
+ let(:password) { 'xxx' }
+
+ context 'when params correct' do
+ let(:token) { 'xxx.token.xxx' }
+
+ let(:secrets_json) do
+ [
+ {
+ 'metadata': {
+ name: metadata_name
+ },
+ 'data': {
+ 'token': Base64.encode64(token)
+ }
+ }
+ ]
+ end
+
+ before do
+ allow_any_instance_of(Kubeclient::Client)
+ .to receive(:get_secrets).and_return(secrets_json)
+ end
+
+ context 'when default-token exists' do
+ let(:metadata_name) { 'default-token-123' }
+
+ it { is_expected.to eq(token) }
+ end
+
+ context 'when default-token does not exist' do
+ let(:metadata_name) { 'another-token-123' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'when api_url is nil' do
+ let(:api_url) { nil }
+
+ it { expect { subject }.to raise_error("Incomplete settings") }
+ end
+
+ context 'when username is nil' do
+ let(:username) { nil }
+
+ it { expect { subject }.to raise_error("Incomplete settings") }
+ end
+
+ context 'when password is nil' do
+ let(:password) { nil }
+
+ it { expect { subject }.to raise_error("Incomplete settings") }
+ end
+ end
+end
diff --git a/spec/services/ci/finalize_cluster_creation_service_spec.rb b/spec/services/ci/finalize_cluster_creation_service_spec.rb
new file mode 100644
index 00000000000..def3709fdb4
--- /dev/null
+++ b/spec/services/ci/finalize_cluster_creation_service_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Ci::FinalizeClusterCreationService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:result) { described_class.new.execute(cluster) }
+
+ context 'when suceeded to get cluster from api' do
+ let(:gke_cluster) { double }
+
+ before do
+ allow(gke_cluster).to receive(:endpoint).and_return('111.111.111.111')
+ allow(gke_cluster).to receive(:master_auth).and_return(spy)
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_get).and_return(gke_cluster)
+ end
+
+ context 'when suceeded to get kubernetes token' do
+ let(:kubernetes_token) { 'abc' }
+
+ before do
+ allow_any_instance_of(Ci::FetchKubernetesTokenService)
+ .to receive(:execute).and_return(kubernetes_token)
+ end
+
+ it 'executes integration cluster' do
+ expect_any_instance_of(Ci::IntegrateClusterService).to receive(:execute)
+ described_class.new.execute(cluster)
+ end
+ end
+
+ context 'when failed to get kubernetes token' do
+ before do
+ allow_any_instance_of(Ci::FetchKubernetesTokenService)
+ .to receive(:execute).and_return(nil)
+ end
+
+ it 'sets an error to cluster object' do
+ described_class.new.execute(cluster)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+
+ context 'when failed to get cluster from api' do
+ let(:error) { Google::Apis::ServerError.new('a') }
+
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_get).and_raise(error)
+ end
+
+ it 'sets an error to cluster object' do
+ described_class.new.execute(cluster)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/integrate_cluster_service_spec.rb b/spec/services/ci/integrate_cluster_service_spec.rb
new file mode 100644
index 00000000000..3a79c205bd1
--- /dev/null
+++ b/spec/services/ci/integrate_cluster_service_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Ci::IntegrateClusterService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster, :custom_project_namespace) }
+ let(:endpoint) { '123.123.123.123' }
+ let(:ca_cert) { 'ca_cert_xxx' }
+ let(:token) { 'token_xxx' }
+ let(:username) { 'username_xxx' }
+ let(:password) { 'password_xxx' }
+
+ before do
+ described_class
+ .new.execute(cluster, endpoint, ca_cert, token, username, password)
+
+ cluster.reload
+ end
+
+ context 'when correct params' do
+ it 'creates a cluster object' do
+ expect(cluster.endpoint).to eq(endpoint)
+ expect(cluster.ca_cert).to eq(ca_cert)
+ expect(cluster.kubernetes_token).to eq(token)
+ expect(cluster.username).to eq(username)
+ expect(cluster.password).to eq(password)
+ expect(cluster.service.active).to be_truthy
+ expect(cluster.service.api_url).to eq(cluster.api_url)
+ expect(cluster.service.ca_pem).to eq(ca_cert)
+ expect(cluster.service.namespace).to eq(cluster.project_namespace)
+ expect(cluster.service.token).to eq(token)
+ end
+ end
+
+ context 'when invalid params' do
+ let(:endpoint) { nil }
+
+ it 'sets an error to cluster object' do
+ expect(cluster).to be_errored
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/provision_cluster_service_spec.rb b/spec/services/ci/provision_cluster_service_spec.rb
new file mode 100644
index 00000000000..5ce5c788314
--- /dev/null
+++ b/spec/services/ci/provision_cluster_service_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Ci::ProvisionClusterService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:operation) { spy }
+
+ shared_examples 'error' do
+ it 'sets an error to cluster object' do
+ described_class.new.execute(cluster)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to request provision' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create).and_return(operation)
+ end
+
+ context 'when operation status is RUNNING' do
+ before do
+ allow(operation).to receive(:status).and_return('RUNNING')
+ end
+
+ context 'when suceeded to parse gcp operation id' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:parse_operation_id).and_return('operation-123')
+ end
+
+ context 'when cluster status is scheduled' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:parse_operation_id).and_return('operation-123')
+ end
+
+ it 'schedules a worker for status minitoring' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+
+ described_class.new.execute(cluster)
+ end
+ end
+
+ context 'when cluster status is creating' do
+ before do
+ cluster.make_creating!
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when failed to parse gcp operation id' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:parse_operation_id).and_return(nil)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when operation status is others' do
+ before do
+ allow(operation).to receive(:status).and_return('others')
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when failed to request provision' do
+ let(:error) { Google::Apis::ServerError.new('a') }
+
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create).and_raise(error)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index fbb3213f42b..9db3568abee 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -20,7 +20,7 @@ describe Ci::RetryBuildService do
erased_at auto_canceled_by].freeze
IGNORE_ACCESSORS =
- %i[type lock_version target_url base_tags
+ %i[type lock_version target_url base_tags trace_sections
commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason].freeze
diff --git a/spec/services/ci/update_cluster_service_spec.rb b/spec/services/ci/update_cluster_service_spec.rb
new file mode 100644
index 00000000000..a289385b88f
--- /dev/null
+++ b/spec/services/ci/update_cluster_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Ci::UpdateClusterService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service) }
+
+ before do
+ described_class.new(cluster.project, cluster.user, params).execute(cluster)
+
+ cluster.reload
+ end
+
+ context 'when correct params' do
+ context 'when enabled is true' do
+ let(:params) { { 'enabled' => 'true' } }
+
+ it 'enables cluster and overwrite kubernetes service' do
+ expect(cluster.enabled).to be_truthy
+ expect(cluster.service.active).to be_truthy
+ expect(cluster.service.api_url).to eq(cluster.api_url)
+ expect(cluster.service.ca_pem).to eq(cluster.ca_cert)
+ expect(cluster.service.namespace).to eq(cluster.project_namespace)
+ expect(cluster.service.token).to eq(cluster.kubernetes_token)
+ end
+ end
+
+ context 'when enabled is false' do
+ let(:params) { { 'enabled' => 'false' } }
+
+ it 'disables cluster and kubernetes service' do
+ expect(cluster.enabled).to be_falsy
+ expect(cluster.service.active).to be_falsy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
index 03c682ae0d7..5a9eb359ee1 100644
--- a/spec/services/delete_merged_branches_service_spec.rb
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe DeleteMergedBranchesService do
+ include ProjectForksHelper
+
subject(:service) { described_class.new(project, project.owner) }
let(:project) { create(:project, :repository) }
@@ -50,9 +52,9 @@ describe DeleteMergedBranchesService do
context 'open merge requests' do
it 'does not delete branches from open merge requests' do
- fork_link = create(:forked_project_link, forked_from_project: project)
+ forked_project = fork_project(project)
create(:merge_request, :opened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master')
- create(:merge_request, :opened, source_project: fork_link.forked_to_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master')
+ create(:merge_request, :opened, source_project: forked_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master')
service.execute
diff --git a/spec/services/discussions/update_diff_position_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb
index 82b156f5ebe..2b84206318f 100644
--- a/spec/services/discussions/update_diff_position_service_spec.rb
+++ b/spec/services/discussions/update_diff_position_service_spec.rb
@@ -164,8 +164,8 @@ describe Discussions::UpdateDiffPositionService do
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
+ expect(change_position.formatter.old_line).to eq(9)
+ expect(change_position.formatter.new_line).to be_nil
end
it 'creates a system discussion' do
@@ -184,7 +184,7 @@ describe Discussions::UpdateDiffPositionService do
expect(discussion.original_position).to eq(old_position)
expect(discussion.position).not_to eq(old_position)
- expect(discussion.position.new_line).to eq(22)
+ expect(discussion.position.formatter.new_line).to eq(22)
end
context 'when the resolve_outdated_diff_discussions setting is set' do
diff --git a/spec/services/emails/confirm_service_spec.rb b/spec/services/emails/confirm_service_spec.rb
new file mode 100644
index 00000000000..2b2c31e2521
--- /dev/null
+++ b/spec/services/emails/confirm_service_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe Emails::ConfirmService do
+ let(:user) { create(:user) }
+
+ subject(:service) { described_class.new(user) }
+
+ describe '#execute' do
+ it 'sends a confirmation email again' do
+ email = user.emails.create(email: 'new@email.com')
+ mail = service.execute(email)
+ expect(mail.subject).to eq('Confirmation instructions')
+ end
+ end
+end
diff --git a/spec/services/emails/create_service_spec.rb b/spec/services/emails/create_service_spec.rb
index 75812c17309..54692c88623 100644
--- a/spec/services/emails/create_service_spec.rb
+++ b/spec/services/emails/create_service_spec.rb
@@ -12,6 +12,11 @@ describe Emails::CreateService do
expect(Email.where(opts)).not_to be_empty
end
+ it 'creates an email with additional attributes' do
+ expect { service.execute(confirmation_token: 'abc') }.to change { Email.count }.by(1)
+ expect(Email.where(opts).first.confirmation_token).to eq 'abc'
+ end
+
it 'has the right user association' do
service.execute
diff --git a/spec/services/emails/destroy_service_spec.rb b/spec/services/emails/destroy_service_spec.rb
index 7726fc0ef81..c3204fac3df 100644
--- a/spec/services/emails/destroy_service_spec.rb
+++ b/spec/services/emails/destroy_service_spec.rb
@@ -4,11 +4,11 @@ describe Emails::DestroyService do
let!(:user) { create(:user) }
let!(:email) { create(:email, user: user) }
- subject(:service) { described_class.new(user, user: user, email: email.email) }
+ subject(:service) { described_class.new(user, user: user) }
describe '#execute' do
it 'removes an email' do
- expect { service.execute }.to change { user.emails.count }.by(-1)
+ expect { service.execute(email) }.to change { user.emails.count }.by(-1)
end
end
end
diff --git a/spec/services/gpg_keys/create_service_spec.rb b/spec/services/gpg_keys/create_service_spec.rb
index 20382a3a618..1cd2625531e 100644
--- a/spec/services/gpg_keys/create_service_spec.rb
+++ b/spec/services/gpg_keys/create_service_spec.rb
@@ -18,4 +18,14 @@ describe GpgKeys::CreateService do
it 'creates a gpg key' do
expect { subject.execute }.to change { user.gpg_keys.where(params).count }.by(1)
end
+
+ context 'when the public key contains subkeys' do
+ let(:params) { attributes_for(:gpg_key_with_subkeys) }
+
+ it 'generates the gpg subkeys' do
+ gpg_key = subject.execute
+
+ expect(gpg_key.subkeys.count).to eq(2)
+ end
+ end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index a8a8aeed1bd..f07b81e842a 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -48,7 +48,8 @@ describe Issues::UpdateService, :mailer do
assignee_ids: [user2.id],
state_event: 'close',
label_ids: [label.id],
- due_date: Date.tomorrow
+ due_date: Date.tomorrow,
+ discussion_locked: true
}
end
@@ -62,6 +63,7 @@ describe Issues::UpdateService, :mailer do
expect(issue).to be_closed
expect(issue.labels).to match_array [label]
expect(issue.due_date).to eq Date.tomorrow
+ expect(issue.discussion_locked).to be_truthy
end
it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do
@@ -110,6 +112,7 @@ describe Issues::UpdateService, :mailer do
expect(issue.labels).to be_empty
expect(issue.milestone).to be_nil
expect(issue.due_date).to be_nil
+ expect(issue.discussion_locked).to be_falsey
end
end
@@ -148,6 +151,13 @@ describe Issues::UpdateService, :mailer do
expect(note).not_to be_nil
expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**'
end
+
+ it 'creates system note about discussion lock' do
+ note = find_note('locked this issue')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq 'locked this issue'
+ end
end
end
@@ -257,6 +267,30 @@ describe Issues::UpdateService, :mailer do
end
end
+ context 'when a new assignee added' do
+ subject { update_issue(assignees: issue.assignees + [user2]) }
+
+ it 'creates only 1 new todo' do
+ expect { subject }.to change { Todo.count }.by(1)
+ end
+
+ it 'creates a todo for new assignee' do
+ subject
+
+ attributes = {
+ project: project,
+ author: user,
+ user: user2,
+ target_id: issue.id,
+ target_type: issue.class.name,
+ action: Todo::ASSIGNED,
+ state: :pending
+ }
+
+ expect(Todo.where(attributes).count).to eq(1)
+ end
+ end
+
context 'when the milestone change' do
it 'marks todos as done' do
update_issue(milestone: create(:milestone))
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index 6f49a65d795..9c9b0c4c4a1 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -1,14 +1,16 @@
require 'spec_helper'
describe MergeRequests::Conflicts::ResolveService do
+ include ProjectForksHelper
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
-
- let(:fork_project) do
- create(:forked_project_with_submodules) do |fork_project|
- fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- fork_project.save
- end
+ let(:project) { create(:project, :public, :repository) }
+
+ let(:forked_project) do
+ forked_project = fork_project(project, user)
+ TestEnv.copy_repo(forked_project,
+ bare_repo: TestEnv.forked_repo_path_bare,
+ refs: TestEnv::FORKED_BRANCH_SHA)
+ forked_project
end
let(:merge_request) do
@@ -19,7 +21,7 @@ describe MergeRequests::Conflicts::ResolveService do
let(:merge_request_from_fork) do
create(:merge_request,
- source_branch: 'conflict-resolvable-fork', source_project: fork_project,
+ source_branch: 'conflict-resolvable-fork', source_project: forked_project,
target_branch: 'conflict-start', target_project: project)
end
@@ -114,7 +116,7 @@ describe MergeRequests::Conflicts::ResolveService do
end
it 'gets conflicts from the source project' do
- expect(fork_project.repository.rugged).to receive(:merge_commits).and_call_original
+ expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original
expect(project.repository.rugged).not_to receive(:merge_commits)
resolve_conflicts
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 25599dea19f..274624aa8bb 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -1,6 +1,8 @@
require "spec_helper"
describe MergeRequests::GetUrlsService do
+ include ProjectForksHelper
+
let(:project) { create(:project, :public, :repository) }
let(:service) { described_class.new(project) }
let(:source_branch) { "merge-test" }
@@ -85,7 +87,7 @@ describe MergeRequests::GetUrlsService do
context 'pushing to existing branch from forked project' do
let(:user) { create(:user) }
- let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+ let!(:forked_project) { fork_project(project, user, repository: true) }
let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) }
let(:changes) { existing_branch_changes }
# Source project is now the forked one
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 64e676f22a0..a2c05761f6b 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe MergeRequests::RefreshService do
+ include ProjectForksHelper
+
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:service) { described_class }
@@ -12,7 +14,8 @@ describe MergeRequests::RefreshService do
group.add_owner(@user)
@project = create(:project, :repository, namespace: group)
- @fork_project = Projects::ForkService.new(@project, @user).execute
+ @fork_project = fork_project(@project, @user, repository: true)
+
@merge_request = create(:merge_request,
source_project: @project,
source_branch: 'master',
@@ -58,7 +61,7 @@ describe MergeRequests::RefreshService do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks)
- .with(@merge_request, 'update', @oldrev)
+ .with(@merge_request, 'update', old_rev: @oldrev)
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
@@ -84,7 +87,7 @@ describe MergeRequests::RefreshService do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks)
- .with(@merge_request, 'update', @oldrev)
+ .with(@merge_request, 'update', old_rev: @oldrev)
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
@@ -179,7 +182,7 @@ describe MergeRequests::RefreshService do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks)
- .with(@fork_merge_request, 'update', @oldrev)
+ .with(@fork_merge_request, 'update', old_rev: @oldrev)
expect(@merge_request.notes).to be_empty
expect(@merge_request).to be_open
@@ -261,7 +264,7 @@ describe MergeRequests::RefreshService do
it 'refreshes the merge request' do
expect(refresh_service).to receive(:execute_hooks)
- .with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA)
+ .with(@fork_merge_request, 'update', old_rev: Gitlab::Git::BLANK_SHA)
allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev)
refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master')
@@ -311,8 +314,7 @@ describe MergeRequests::RefreshService do
context 'when the merge request is sourced from a different project' do
it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
- forked_project = create(:project, :repository)
- create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project)
+ forked_project = fork_project(@project, @user, repository: true)
merge_request = create(:merge_request,
target_branch: 'master',
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 681feee61d1..98409be4236 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -49,7 +49,8 @@ describe MergeRequests::UpdateService, :mailer do
state_event: 'close',
label_ids: [label.id],
target_branch: 'target',
- force_remove_source_branch: '1'
+ force_remove_source_branch: '1',
+ discussion_locked: true
}
end
@@ -73,11 +74,13 @@ describe MergeRequests::UpdateService, :mailer do
expect(@merge_request.labels.first.title).to eq(label.name)
expect(@merge_request.target_branch).to eq('target')
expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1')
+ expect(@merge_request.discussion_locked).to be_truthy
end
it 'executes hooks with update action' do
- expect(service).to have_received(:execute_hooks)
- .with(@merge_request, 'update')
+ expect(service)
+ .to have_received(:execute_hooks)
+ .with(@merge_request, 'update', old_labels: [], old_assignees: [user3])
end
it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
@@ -123,6 +126,13 @@ describe MergeRequests::UpdateService, :mailer do
expect(note.note).to eq 'changed target branch from `master` to `target`'
end
+ it 'creates system note about discussion lock' do
+ note = find_note('locked this merge request')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq 'locked this merge request'
+ end
+
context 'when not including source branch removal options' do
before do
opts.delete(:force_remove_source_branch)
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index f4b36eb7eeb..b64ca5be8fc 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -105,18 +105,6 @@ describe NotificationService, :mailer do
end
end
- describe 'Email' do
- describe '#new_email' do
- let!(:email) { create(:email) }
-
- it { expect(notification.new_email(email)).to be_truthy }
-
- it 'sends email to email owner' do
- expect { notification.new_email(email) }.to change { ActionMailer::Base.deliveries.size }.by(1)
- end
- end
- end
-
describe 'Notes' do
context 'issue note' do
let(:project) { create(:project, :private) }
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index c867139d1de..c90bad46295 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -212,6 +212,19 @@ describe Projects::DestroyService do
end
end
+ context 'as the root of a fork network' do
+ let!(:fork_network) { create(:fork_network, root_project: project) }
+
+ it 'updates the fork network with the project name' do
+ destroy_project(project, user)
+
+ fork_network.reload
+
+ expect(fork_network.deleted_root_project_name).to eq(project.full_name)
+ expect(fork_network.root_project).to be_nil
+ end
+ end
+
def destroy_project(project, user, params = {})
if async
Projects::DestroyService.new(project, user, params).async_execute
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index fa9d6969830..53862283a27 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Projects::ForkService do
+ include ProjectForksHelper
let(:gitlab_shell) { Gitlab::Shell.new }
describe 'fork by user' do
@@ -33,7 +34,7 @@ describe Projects::ForkService do
end
describe "successfully creates project in the user namespace" do
- let(:to_project) { fork_project(@from_project, @to_user) }
+ let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) }
it { expect(to_project).to be_persisted }
it { expect(to_project.errors).to be_empty }
@@ -60,13 +61,40 @@ describe Projects::ForkService do
expect(@from_project.forks_count).to eq(1)
end
+
+ it 'creates a fork network with the new project and the root project set' do
+ to_project
+ fork_network = @from_project.reload.fork_network
+
+ expect(fork_network).not_to be_nil
+ expect(fork_network.root_project).to eq(@from_project)
+ expect(fork_network.projects).to contain_exactly(@from_project, to_project)
+ end
+ end
+
+ context 'creating a fork of a fork' do
+ let(:from_forked_project) { fork_project(@from_project, @to_user) }
+ let(:other_namespace) do
+ group = create(:group)
+ group.add_owner(@to_user)
+ group
+ end
+ let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
+
+ it 'sets the root of the network to the root project' do
+ expect(to_project.fork_network.root_project).to eq(@from_project)
+ end
+
+ it 'sets the forked_from_project on the membership' do
+ expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project)
+ end
end
end
context 'project already exists' do
it "fails due to validation, not transaction failure" do
@existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
- @to_project = fork_project(@from_project, @to_user)
+ @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace)
expect(@existing_project).to be_persisted
expect(@to_project).not_to be_persisted
@@ -88,7 +116,7 @@ describe Projects::ForkService do
end
it 'does not allow creation' do
- to_project = fork_project(@from_project, @to_user)
+ to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace)
expect(to_project).not_to be_persisted
expect(to_project.errors.messages).to have_key(:base)
@@ -182,9 +210,4 @@ describe Projects::ForkService do
end
end
end
-
- def fork_project(from_project, user, params = {})
- allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
- Projects::ForkService.new(from_project, user, params).execute
- end
end
diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage_migration_service_spec.rb
index 1b61207b550..aa1988d29d6 100644
--- a/spec/services/projects/hashed_storage_migration_service_spec.rb
+++ b/spec/services/projects/hashed_storage_migration_service_spec.rb
@@ -20,7 +20,7 @@ describe Projects::HashedStorageMigrationService do
expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy
end
- it 'updates project to be hashed and not readonly' do
+ it 'updates project to be hashed and not read-only' do
service.execute
expect(project.hashed_storage?).to be_truthy
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
index 4f1ab697460..50d3a4ec982 100644
--- a/spec/services/projects/unlink_fork_service_spec.rb
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -1,19 +1,22 @@
require 'spec_helper'
describe Projects::UnlinkForkService do
- subject { described_class.new(fork_project, user) }
+ include ProjectForksHelper
- let(:fork_link) { create(:forked_project_link) }
- let(:fork_project) { fork_link.forked_to_project }
+ subject { described_class.new(forked_project, user) }
+
+ let(:fork_link) { forked_project.forked_project_link }
+ let(:project) { create(:project, :public) }
+ let(:forked_project) { fork_project(project, user) }
let(:user) { create(:user) }
context 'with opened merge request on the source project' do
- let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) }
- let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) }
+ let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: fork_link.forked_from_project) }
+ let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) }
before do
allow(MergeRequests::CloseService).to receive(:new)
- .with(fork_project, user)
+ .with(forked_project, user)
.and_return(mr_close_service)
end
@@ -25,13 +28,24 @@ describe Projects::UnlinkForkService do
end
it 'remove fork relation' do
- expect(fork_project.forked_project_link).to receive(:destroy)
+ expect(forked_project.forked_project_link).to receive(:destroy)
+
+ subject.execute
+ end
+
+ it 'removes the link to the fork network' do
+ expect(forked_project.fork_network_member).to be_present
+ expect(forked_project.fork_network).to be_present
subject.execute
+ forked_project.reload
+
+ expect(forked_project.fork_network_member).to be_nil
+ expect(forked_project.reload.fork_network).to be_nil
end
it 'refreshes the forks count cache of the source project' do
- source = fork_project.forked_from_project
+ source = forked_project.forked_from_project
expect(source.forks_count).to eq(1)
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index d400304622e..3da222e2ed8 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::UpdateService, '#execute' do
+ include ProjectForksHelper
+
let(:gitlab_shell) { Gitlab::Shell.new }
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -76,13 +78,7 @@ describe Projects::UpdateService, '#execute' do
describe 'when updating project that has forks' do
let(:project) { create(:project, :internal) }
- let(:forked_project) { create(:forked_project_with_submodules, :internal) }
-
- before do
- forked_project.build_forked_project_link(forked_to_project_id: forked_project.id,
- forked_from_project_id: project.id)
- forked_project.save
- end
+ let(:forked_project) { fork_project(project) }
it 'updates forks visibility level when parent set to more restrictive' do
opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index b1241cd8d0b..cd473c1f388 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -1145,4 +1145,42 @@ describe SystemNoteService do
it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" }
end
end
+
+ describe '.discussion_lock' do
+ subject { described_class.discussion_lock(noteable, author) }
+
+ context 'discussion unlocked' do
+ it_behaves_like 'a system note' do
+ let(:action) { 'unlocked' }
+ end
+
+ it 'creates the note text correctly' do
+ [:issue, :merge_request].each do |type|
+ issuable = create(type)
+
+ expect(described_class.discussion_lock(issuable, author).note)
+ .to eq("unlocked this #{type.to_s.titleize.downcase}")
+ end
+ end
+ end
+
+ context 'discussion locked' do
+ before do
+ noteable.update_attribute(:discussion_locked, true)
+ end
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'locked' }
+ end
+
+ it 'creates the note text correctly' do
+ [:issue, :merge_request].each do |type|
+ issuable = create(type, discussion_locked: true)
+
+ expect(described_class.discussion_lock(issuable, author).note)
+ .to eq("locked this #{type.to_s.titleize.downcase}")
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb
index fef4da0c76e..17eabad73be 100644
--- a/spec/services/users/activity_service_spec.rb
+++ b/spec/services/users/activity_service_spec.rb
@@ -38,6 +38,18 @@ describe Users::ActivityService do
end
end
end
+
+ context 'when in GitLab read-only instance' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ end
+
+ it 'does not update last_activity_at' do
+ service.execute
+
+ expect(last_hour_user_ids).to eq([])
+ end
+ end
end
def last_hour_user_ids
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 576e4ae1d38..48cacba6a8a 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -81,7 +81,10 @@ RSpec.configure do |config|
if ENV['CI']
# This includes the first try, i.e. tests will be run 4 times before failing.
config.default_retry_count = 4
- config.reporter.register_listener(RspecFlaky::Listener.new, :example_passed, :dump_summary)
+ config.reporter.register_listener(
+ RspecFlaky::Listener.new,
+ :example_passed,
+ :dump_summary)
end
config.before(:suite) do
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 81cb94ab8c4..9f05cabf7ae 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -71,7 +71,7 @@ shared_examples 'discussion comments' do |resource_name|
expect(page).not_to have_selector menu_selector
find(toggle_selector).click
- find('body').click
+ find('body').trigger 'click'
expect(page).not_to have_selector menu_selector
end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 8282ba7e536..061e0d35590 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -29,7 +29,7 @@ shared_examples 'issuable record that supports quick actions in its description
wait_for_requests
end
- describe "new #{issuable_type}", js: true do
+ describe "new #{issuable_type}", :js do
context 'with commands in the description' do
it "creates the #{issuable_type} and interpret commands accordingly" do
case issuable_type
@@ -53,7 +53,7 @@ shared_examples 'issuable record that supports quick actions in its description
end
end
- describe "note on #{issuable_type}", js: true do
+ describe "note on #{issuable_type}", :js do
before do
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
end
@@ -290,7 +290,7 @@ shared_examples 'issuable record that supports quick actions in its description
end
end
- describe "preview of note on #{issuable_type}", js: true do
+ describe "preview of note on #{issuable_type}", :js do
it 'removes quick actions from note and explains them' do
create(:user, username: 'bob')
diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb
index 65b38626a51..3f7279a50e0 100644
--- a/spec/support/gpg_helpers.rb
+++ b/spec/support/gpg_helpers.rb
@@ -92,6 +92,46 @@ module GpgHelpers
KEY
end
+ def public_key_with_extra_signing_key
+ <<~KEY.strip
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+ Version: GnuPG v1
+
+ mI0EWK7VJwEEANSFayuVYenl7sBKUjmIxwDRc3jd+K+FWUZgknLgiLcevaLh/mxV
+ 98dLxDKGDHHNKc/B7Y4qdlZYv1wfNQVuIbd8dqUQFOOkH7ukbgcGBTxH+2IM67y+
+ QBH618luS5Gz1d4bd0YoFf/xZGEh9G5xicz7TiXYzLKjnMjHu2EmbFePABEBAAG0
+ LU5hbm5pZSBCZXJuaGFyZCA8bmFubmllLmJlcm5oYXJkQGV4YW1wbGUuY29tPoi4
+ BBMBAgAiBQJYrtUnAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDM++Gf
+ AKyLHaeSA/99oUWpb02PlfkALcx5RncboMHkgczYEU9wOFIgvXIReswThCMOvPZa
+ piui+ItyJfV3ijJfO8IvbbFcvU7jjGA073Bb7tbzAEOQLA16mWgBLQlGaRWbHDW4
+ uwFxvkJKA0GzEsadEXeniESaZPc4rOXKPO3+/MSQWS2bmvvGsBTEuriNBFiu1ScB
+ BADIXkITf+kKCkD+n8tMsdTLInefu8KrJ8p7YRYCCabEXnWRsDb5zxUAG2VXCVUh
+ Yl6QXQybkNiBaduS+uxilz7gtYZUMFJvQ09+fV7D2N9B7u/1bGdIYz+cDFJnEJit
+ LY4w/nju2Sno5CL5Ead8sZuslKetSXPYHR/kbW462EOw5wARAQABiJ8EGAECAAkF
+ Aliu1ScCGwwACgkQzPvhnwCsix2WRQQAtOXpBS60myrBUXhlcqabDQgSTw+Spbgb
+ 61hEMckyzpk7SfMNLz0EbYMvj9SU6znBG8RGeUljPTVMxPGr9yIpoFMSPKAUi/0K
+ AgRmH3tVpxlMipwXjST1Jukk2eHckt/3jGw3E1ElMSFtULe6u5p4gu578hHukEwT
+ IKzj0ZyC7DK5AQ0EWcx23AEIANwpAq85bT10JCBuNhOMyF2jKVt5wHbI9wBtjWYG
+ fgJFBkRvm6IsbmR0Y5DSBvF/of0UX1iGMfx6mvCDJkb1okquhCUef6MONWRpzXYE
+ CIZDm1TXu6yv0D35tkLfPo+/sY9UHHp1zGRcPAU46e8ztRwoD+zEJwy7lobLHGOL
+ 9OdWtCGjsutLOTqKRK4jsifr8n3rePU09rejhDkRONNs7ufn9GRcWMN7RWiFDtpU
+ gNe84AJ38qaXPU8GHNTrDtDtRRPmn68ezMmE1qTNsxQxD4Isexe5Wsfc4+ElaP9s
+ zaHgij7npX1HS9RpmhnOa2h1ESroM9cqDh3IJVhf+eP6/uMAEQEAAYkBxAQYAQIA
+ DwUCWcx23AIbAgUJAeEzgAEpCRDM++GfAKyLHcBdIAQZAQIABgUCWcx23AAKCRDk
+ garE0uOuES7DCAC2Kgl6zO+NqIBIS6McgcEN0sGyvOvZ8Ps4hBiMwCyDAnsIRAUi
+ v4KZMtQMAyl9njJ3YjPWBsdieuTz45O06DDnrzJpZO5rUGJjAcEue4zvRRWIyu3H
+ qHC8MsvkslsNCygJHoWlknm+HucroskTNtxHQ+FdKZ6Tey+twl1u+PhV8PQVyFkl
+ 4G1chO90EP4dvYrye26CC+ik2JkvC7Vy5M+U0PJikme8pFMjcdNks25BnAKcdqKU
+ AU8RTkSjoYvb8qSmZyldJjYjQRkTPRX1ZdaOID1EdiWl+s5cn0Oypo3z7BChcEMx
+ IWB/gmXQJQXVr5qNQnJObyMO/RczXYi9qNnyGMED/2EJJERLR0nefjHQalwDKQVP
+ s5lX1OKAcf2CrV6ZarckqaQgtqjZbuV2C2mrOHUs5uojlXaopj5gA4yJSGDcYhj1
+ Rg9jdHWBtkHBj3eL32ZqrHDs3ap8ErZMmPE8A+mn9TTnQS+FY2QF7vBjJKM3qPT7
+ DMVGWrg4m1NF8N6yMPMP
+ =RB1y
+ -----END PGP PUBLIC KEY BLOCK-----
+ KEY
+ end
+
def primary_keyid
fingerprint[-16..-1]
end
@@ -201,4 +241,277 @@ module GpgHelpers
['bette.cartwright@example.com', 'bette.cartwright@example.net']
end
end
+
+ # GPG Key with extra signing key
+ module User3
+ extend self
+
+ def signed_commit_signature
+ <<~SIGNATURE
+ -----BEGIN PGP SIGNATURE-----
+
+ iQEzBAABCAAdFiEEBSLdKbmPFnzYQhdS44/8r3Wr2SoFAlnNlT8ACgkQ44/8r3Wr
+ 2SqP1wf9FC4J2S8LIHs/fpxgkYzsyCp5lCbS7JuoD4pqmI2KWyBx+vi9/3mZPCsm
+ Fj9f0vFEtNOb39GNGZbaA8DdGw30/WAS6kI6yco0WSK53KHrLw9Kqd+3e/NAVSsl
+ 991Gq4n8X1U5izSH+gZOMtEEUBGqIlZKgRrEh7lhNcz0G7JTF2VCE4NNtZdq7GDA
+ N6jOQxDGUwi9wQBYORQzIBc3NihfhGloII1hXf0XzrgUY3zNYHTT7QipCxKAmH54
+ skwW+wi8RpBedar4saf7fs5xZbP/0yyVz98MJMdHBL68++Xt1AIHoqrb7eWszqnd
+ PCo/fnz1iHKCig602KLj0/zhADcNkg==
+ =LsTi
+ -----END PGP SIGNATURE-----
+ SIGNATURE
+ end
+
+ def signed_commit_base_data
+ <<~SIGNEDDATA
+ tree 86ec18bfe87ad42a782fdabd8310f9b7ac750f51
+ parent 2d1096e3a0ecf1d2baf6dee036cc80775d4940ba
+ author John Doe <john.doe@example.com> 1506645311 -0500
+ committer John Doe <john.doe@example.com> 1506645311 -0500
+
+ Commit signed with subkey by John Doe
+ SIGNEDDATA
+ end
+
+ def public_key
+ <<~KEY.strip
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+ mQENBFnNgbIBCAC9/WblcR4s/pFTwh9cm2iS59YRhtGfbrnfNE6QMIFIRFaK0u6J
+ LDy+scipXLoGg7X0XNFLW6Y457UUpkaPDVLPuVMONhMXdVqndGVvk9jq3D4oDtRa
+ ukucuXr9F84lHnjL3EosmAUiK3xBmHOGHm8+bvKbgPOdvre48YxoJ1POTen+chfo
+ YtLUfnC9EEGY/bn00aaXm3NV+zZK2zio5bFX9YLWSNh/JaXxuJsLoHR/lVrU7CLt
+ FCaGcPQ9SU46LHPshEYWO7ZsjEYJsYYOIOEzfcfR47T2EDTa6mVc++gC5TCoY3Ql
+ jccgm+EM0LvyEHwupEpxzCg2VsT0yoedhUhtABEBAAG0H0pvaG4gRG9lIDxqb2hu
+ LmRvZUBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQTqP4uIlyqP1HSHLy8RWzrxqtPt
+ ugUCWc2BsgIbAwUJA8JnAAULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRARWzrx
+ qtPturMDCACc1Pi1sLJFcCnJEc9sCInCO4LH8fntNjRLN3MTPU5YCYmFM3fAl5ly
+ vXPZ4jNWZxKbQVeFnkDOg5Ti8bzmFEMc8KbZuguktVFizxnLdFCTTRO89i3HDVSN
+ bIosrs5HJwRKOzul6i2whn3dsr8/P8WJczYjZGiw29hGwH3md4Thn/aAGbzjoCEF
+ IfIb1kccyHMJkaj79S8B2agsbEJLuTSfsXC3kGZIJyKG1DNmDUHW/ZE6ro/Kkhik
+ 3w6Jw14cMsKUIOBkOgsD/gXgX9xxWjYHmKrbCXucTKUevNlaCy5tzwrC0Am3prl9
+ OJj3macOA8hNaTVDflEHNCwHOwqnVQYyuQENBFnNgbIBCAC59MmKc0cqPRPTpCZ5
+ arKRoj23SNKWMDWaxSELdU91Wd/NJk4wF25urk9BtBuwjzaBMqb/xPD88yJC3ucs
+ 2LwCyAb5C/dHcPOpys8Pd+KrdHDR3zAMjcASsizlW/qFI9MtjhcU9Yd6iTUejAZG
+ NEC76WALJ3SLYzCaDkHFjWpH3Xq6ck3/9jpL3csn/5GLCsZQUDYGrZSXvHAIigwW
+ Xo6tMs5LCCO9CZg2qGDpvqlzcmy6CRkf0h/UFYJzGqfbJtxeCIxa93WIPE8eGwao
+ aneDlNtIoYiP6krC3OLsaPWT58QltNKaQuZSpjwtQBHa4JIt55vx+FcvRb7Kflgf
+ fT8bABEBAAGJATwEGAEIACYWIQTqP4uIlyqP1HSHLy8RWzrxqtPtugUCWc2BsgIb
+ DAUJA8JnAAAKCRARWzrxqtPtuqJjCACj+Z4qtgMpJXx3u58wCzkVLl5IylD/tEPA
+ cNIrj8QS8ec+woTJaMGVCh96VC2FPl8KR4Hjhy0yaupyPbTI6VWib63S/NcDfG7r
+ tviRFG2Gf8yduERebyC0cpgnmjVgFfJs7N3K3ncz6myOr9idNI05OC9poL73sDUv
+ jRXhm7uy938bT/R4MQdpYuxucgU3MiwvfG5ht+oJ4Yp+/IrR2PTqRGqMCsodnroa
+ TBKq2kW565TtCvrFkNub/ytorDbIVN9VrIMcuTiOv8sLoQRDJO9HvWUhYAqMY6Uh
+ dy12jR9FrquZnGsDKKs9V0Y6J4Wi8vnmdgWVZUc40pfXq3APAB6suQENBFnNgeAB
+ CADFqQRxLHxLIQ7B72diTMI2tPk9d5c67k+Gzkrg1QYoxBLdRCmhM4ydYqZzvIz4
+ 1npkK20w4somOCwvyAOjO46IGb3f/Wk8mY8o5HMpI1miAfze0YTZKzRo2DmrvwbV
+ /h8jvZNCISwtrOgaaszWSVSuEQQCA1jf4qixfCb3ReETvZc3MTZXhw8IUbszXh5d
+ a6CYqq/fr5Zw4Dc19ZSoHSTh0Wn03mEm/kaYtia/wt1I+xVvTSaC2Pf/kUtr7UEf
+ 3NMc0YF0s4KgeW8KwjHa7Sz9wzydLPRH5kJ26SDUGUhrFf1jNLIegtDsoISV/86G
+ ztXcVq5GY6lXMwmsggRe++7xABEBAAGJAmwEGAEIACAWIQTqP4uIlyqP1HSHLy8R
+ WzrxqtPtugUCWc2B4AIbAgFACRARWzrxqtPtusB0IAQZAQgAHRYhBAUi3Sm5jxZ8
+ 2EIXUuOP/K91q9kqBQJZzYHgAAoJEOOP/K91q9kqlHcH+wbvD14ITYDMfgIfy67O
+ 4Qcmgf1qzGXhpsABz/i/EPgRD990eNBI0YGuvoKRJfetEGn7LerrrCB8Z+ICFPHF
+ rzXoe10zm+QTREck0OB8nPFRycJ+Fbl6JX+cnkEx27Mmg1aVb7+H5LMDaWO1KjLs
+ g2wIDo/jrDfW7NoZzy4XTd7jFCOt4fftL/dhiujQmhLzugZXCxRISOVdmgilDJQo
+ Tz1sEm34ida98JFjdzSgkUvJ/pFTZ21ThCNxlUf01Hr2Pdcg1e2/97sZocMFTY2J
+ KwmiW2LG3B05/VrRtdvsCVj8G49coxgPPKty+m71ovAXdS/CvVqE7TefCplsYJ1d
+ V3abwwf/Xl2SxzbAKbrYMgZfdGzpPg2u6982WvfGIVfjFJh9veHZAbfkPcjdAD2X
+ e67Y4BeKG2OQQqeOY2y+81A7PaehgHzbFHJG/4RjqB66efrZAg4DgIdbr4oyMoUJ
+ VVsl0wfYSIvnd4kvWXYICVwk53HLA3wIowZAsJ1LT2byAKbUzayLzTekrTzGcwQk
+ g2XT798ev2QuR5Ki5x8MULBFX4Lhd03+uGOxjhNPQD6DAAHCQLaXQhaGuyMgt5hD
+ t0nF3yuw3Eg4Ygcbtm24rZXOHJc9bDKeRiWZz1dIyYYVJmHSQwOVXkAwJlF1sIgy
+ /jQYeOgFDKq20x86WulkvsUtBoaZJg==
+ =Q5Z7
+ -----END PGP PUBLIC KEY BLOCK-----
+ KEY
+ end
+
+ def secret_key
+ <<~SECRET
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+ lQPGBFnNgbIBCAC9/WblcR4s/pFTwh9cm2iS59YRhtGfbrnfNE6QMIFIRFaK0u6J
+ LDy+scipXLoGg7X0XNFLW6Y457UUpkaPDVLPuVMONhMXdVqndGVvk9jq3D4oDtRa
+ ukucuXr9F84lHnjL3EosmAUiK3xBmHOGHm8+bvKbgPOdvre48YxoJ1POTen+chfo
+ YtLUfnC9EEGY/bn00aaXm3NV+zZK2zio5bFX9YLWSNh/JaXxuJsLoHR/lVrU7CLt
+ FCaGcPQ9SU46LHPshEYWO7ZsjEYJsYYOIOEzfcfR47T2EDTa6mVc++gC5TCoY3Ql
+ jccgm+EM0LvyEHwupEpxzCg2VsT0yoedhUhtABEBAAH+BwMCOqjIWtWBMo3mjIP1
+ OnIbZ+YJxSUZ/B8wU2loMv4XiKmeXLbjD6h3jojxYlnreSHA9QvoY8uNaWElL/n2
+ jv6bxluivk8tA9FWJVv4HaSlMDd2J2YmUW17r8z9Kvm7b7pFVSrEoYV93Wdj5FJ7
+ ciKrFhYNSD7tH1sHwkrFAbiv6aHyk9h48YmR3kx2wBvz+pWk7M2srCJx2b6DXkj/
+ fsj1c/vnzUUGooOJgOvYAWrpg/rJUNxSsFypAHf8Xtk+xt8S1aZ9jaCmYk6I1B2L
+ m00HP43cXUpKcmETW1zXvfMLKjjoUEAJhSJhbCwiEzGL4ojQTarl8qbb+MisakEJ
+ DkPYtrhiiuVzUIFfqE86yO0UKidtzBmJAW3c6zeiUATvACzU09aGyUY1cJi93oXD
+ w4PCyVZ+nMvGD1wx+gyYdDINwpX4y6od9RDr06DGCzwu+S2vxsu1T8LdSv52fhBr
+ U0FY3Z3VN1ytay4SHi/8Y9VBYQFBh7R7Ch0gEMxLVKXVNqOXHUdGrKWV/WmyLKuZ
+ W9DEnWU4Mpz/di5jU8EDW7EB9DZZhVk3mQw3nuAZrBGD4azmmD5mgSgLeBGmKZ1e
+ q/9IWO44mRBBUtNv+rAkmmYF3MCNHuc7EMj+c/IgBUC7d5qBzGWA3UJ0vKX4xcIQ
+ X/PnU+nGxNvBrdqQaMLczeg28SerojxuX79prOsoySctLAbajd9HshW5SfOZ0rvb
+ BNHPqolQDijYEHGxANh4BbamRMGi60Rop7vJsZOLAemz17x/mvCtAHISOJT77/IM
+ oWC+IksJ5XsA/klJGe/tkx11aRQDDmKvIJXmMuRdvnIR23UBbzRQlWWq0l6CdoF6
+ 6SQ9BJBFq0WY32No9WZAPnDO3buUzWc1Y3uwn/+h7TVYVyTlEqzpYJ9FoJwBHbor
+ 0663eoyz6+AUtB9Kb2huIERvZSA8am9obi5kb2VAZXhhbXBsZS5jb20+iQFUBBMB
+ CAA+FiEE6j+LiJcqj9R0hy8vEVs68arT7boFAlnNgbICGwMFCQPCZwAFCwkIBwIG
+ FQgJCgsCBBYCAwECHgECF4AACgkQEVs68arT7bqzAwgAnNT4tbCyRXApyRHPbAiJ
+ wjuCx/H57TY0SzdzEz1OWAmJhTN3wJeZcr1z2eIzVmcSm0FXhZ5AzoOU4vG85hRD
+ HPCm2boLpLVRYs8Zy3RQk00TvPYtxw1UjWyKLK7ORycESjs7peotsIZ93bK/Pz/F
+ iXM2I2RosNvYRsB95neE4Z/2gBm846AhBSHyG9ZHHMhzCZGo+/UvAdmoLGxCS7k0
+ n7Fwt5BmSCcihtQzZg1B1v2ROq6PypIYpN8OicNeHDLClCDgZDoLA/4F4F/ccVo2
+ B5iq2wl7nEylHrzZWgsubc8KwtAJt6a5fTiY95mnDgPITWk1Q35RBzQsBzsKp1UG
+ Mp0DxgRZzYGyAQgAufTJinNHKj0T06QmeWqykaI9t0jSljA1msUhC3VPdVnfzSZO
+ MBdubq5PQbQbsI82gTKm/8Tw/PMiQt7nLNi8AsgG+Qv3R3DzqcrPD3fiq3Rw0d8w
+ DI3AErIs5Vv6hSPTLY4XFPWHeok1HowGRjRAu+lgCyd0i2Mwmg5BxY1qR916unJN
+ //Y6S93LJ/+RiwrGUFA2Bq2Ul7xwCIoMFl6OrTLOSwgjvQmYNqhg6b6pc3JsugkZ
+ H9If1BWCcxqn2ybcXgiMWvd1iDxPHhsGqGp3g5TbSKGIj+pKwtzi7Gj1k+fEJbTS
+ mkLmUqY8LUAR2uCSLeeb8fhXL0W+yn5YH30/GwARAQAB/gcDAuYn/gmAA3OC5p5Q
+ Pat5kE7MtmSvSPmdjVA2o+6RtqZf81JqtAgtDVDwj7SPFsH6ue5P+iAn9938YYek
+ WQU2+0GXeUbSJt+u4VAchgwA5mYsEnEr1/E5KEfWPWO3jJol1rJG99adrjkMxvug
+ QJmwieqhu0368w1FU0tKstxYbr3Tz3nPCPDJoigMEUkXiFklDCUgeNk0g+zd5ytE
+ lXuuLYcGZX7njxL5jD+cMIKqua5zv8WbvNf/BhM1nCarxp4qzKWim8J8jY+iR+/E
+ qOar4aliGRez0j+qh/r8ywgPwfOO89zrKrMfaclL7dN9yuecmBHKWZvfrP5JKMHj
+ yTU3nRMhUGbfVUaaZI2Ccz2rNOU4oF9wuzpzQi8qOysZixRmH61Nw3ULIKoQgiWp
+ 0p5A3L94OaEfZEq3plVaIXI2YWYFSEAlIAc2dq4GxynousLdhNACi9bHhXrfFUhK
+ ckw1QlbhguO/j63/x8ygsmLZVwHG0fJZtMhT3+EGam9cuMBibIYyu3ufJRy7kMKt
+ kmyuk02X+hYJ7w8Pu6b8zYHBXbsEKamipMgd4oKtc8WnXILZo4lwDSArgs7ZVCBa
+ vGBbpTOsr54WjsyuCdX/wv0F2l31J87UxVtTKXx/+nfMfCE02zd+NsTgqvgqmkaA
+ Sy3qvv326kJNx7p+5hRwDzlAZ7vGJjj5TwCbGYDvctIf6MFrGDRNYwrGwNkPc3TG
+ rturfeL/ioua0Smj8LIbOv9Ir93gUIseNpxv8tXV/lffdIplcw802b3aXIKyv4fq
+ b9y3Oq/pDHFukKuBe9WTXJvjT0+ME+a0C8KIb/sts95pmjZsgN1kPmvuT0ReQwUR
+ eGrqz387bnVUzo4RgM3IERs/0EYzPzE8A2vc1e4/87b5J+1Xnov8Phd29vW8Td5l
+ ApiFrFO2r+/Np4kBPAQYAQgAJhYhBOo/i4iXKo/UdIcvLxFbOvGq0+26BQJZzYGy
+ AhsMBQkDwmcAAAoJEBFbOvGq0+26omMIAKP5niq2AyklfHe7nzALORUuXkjKUP+0
+ Q8Bw0iuPxBLx5z7ChMlowZUKH3pULYU+XwpHgeOHLTJq6nI9tMjpVaJvrdL81wN8
+ buu2+JEUbYZ/zJ24RF5vILRymCeaNWAV8mzs3credzPqbI6v2J00jTk4L2mgvvew
+ NS+NFeGbu7L3fxtP9HgxB2li7G5yBTcyLC98bmG36gnhin78itHY9OpEaowKyh2e
+ uhpMEqraRbnrlO0K+sWQ25v/K2isNshU31Wsgxy5OI6/ywuhBEMk70e9ZSFgCoxj
+ pSF3LXaNH0Wuq5mcawMoqz1XRjonhaLy+eZ2BZVlRzjSl9ercA8AHqydA8YEWc2B
+ 4AEIAMWpBHEsfEshDsHvZ2JMwja0+T13lzruT4bOSuDVBijEEt1EKaEzjJ1ipnO8
+ jPjWemQrbTDiyiY4LC/IA6M7jogZvd/9aTyZjyjkcykjWaIB/N7RhNkrNGjYOau/
+ BtX+HyO9k0IhLC2s6BpqzNZJVK4RBAIDWN/iqLF8JvdF4RO9lzcxNleHDwhRuzNe
+ Hl1roJiqr9+vlnDgNzX1lKgdJOHRafTeYSb+Rpi2Jr/C3Uj7FW9NJoLY9/+RS2vt
+ QR/c0xzRgXSzgqB5bwrCMdrtLP3DPJ0s9EfmQnbpINQZSGsV/WM0sh6C0OyghJX/
+ zobO1dxWrkZjqVczCayCBF777vEAEQEAAf4HAwKESvCIDq5QNeadnSvpkZemItPO
+ lDf+7Wiue2gt776D5xkVyT7WkgTQv+IGWGtqz7pCCO2rMp/F9u1BghdjY46jtrK6
+ MMFKta4YENUhRliH6M2YmRjq5p7xZgH6UOnDlqsafbfyUx30t59tbQj+07aMnH5J
+ LMm37nVkDvo3wpPQPuo7L6qizYsrHrQKeJZ8636u41UjC99lVH7vXzqXw68FJImi
+ XdMZbEVBIprYfCDem+fD6gJBA4JBqWJMxuFMfhWp+1WtYoeNojDm4KxBzc2fvYV/
+ HOIUfLFBvACD/UwU5ovllHN39/O8SMgyLm9ymx2/qXcdIkUz4l7fhOCY1OW12DMu
+ 5OFrrTteGK/Sj4Z8pYRdMdaKyjIlxuVzEQGWsU5+J2ALao5atEHguqwlD3cKh3G8
+ 1sA/l5eTFDt84erYv1MVStV0BhZaCE4mNL4WpnQGDdW05yoGq9jIyLcurb/k/atU
+ TUkAF1csgNlJlR3IP+7U9xfHkjMO5+SV82xoNf9nBjz06TRdnvOSKsMNKp0RxC/L
+ Hbiee9o7Rxqdiyv0ly6bCCymwfvlsEIqo3YKssBfe3XI5yQI2hF9QZaH1ywzmgaH
+ o+rbME/XxddRJueS79eipT7K05Z3ulSHTXzpDw+jZcdUV0Ac72Q9FTDPMl3xc6NW
+ DrYwWw/3+kyZ4SkP56l7KlGczTyNPvU9iou4Cj/cAZk/pHx68Chq8ZZNznFm/bIF
+ gWt3fqE/n+y78B6MI8qTjGJOR0jycxrLH82Z2F+FpMShI2C5NnOa/Ilkv3e2Q5U6
+ MOAwaCIz6RHhcI99O/yta2vLelWZqn2g86rLzTG0HlIABTCPYotwetHh0hsrkSv9
+ Kh6rOzGB4i8lRqcBVY+alMSiRBlzkwpL4YUcO6f3vEDncQ9evE1AQCpD4jUJjB1H
+ JSSJAmwEGAEIACAWIQTqP4uIlyqP1HSHLy8RWzrxqtPtugUCWc2B4AIbAgFACRAR
+ WzrxqtPtusB0IAQZAQgAHRYhBAUi3Sm5jxZ82EIXUuOP/K91q9kqBQJZzYHgAAoJ
+ EOOP/K91q9kqlHcH+wbvD14ITYDMfgIfy67O4Qcmgf1qzGXhpsABz/i/EPgRD990
+ eNBI0YGuvoKRJfetEGn7LerrrCB8Z+ICFPHFrzXoe10zm+QTREck0OB8nPFRycJ+
+ Fbl6JX+cnkEx27Mmg1aVb7+H5LMDaWO1KjLsg2wIDo/jrDfW7NoZzy4XTd7jFCOt
+ 4fftL/dhiujQmhLzugZXCxRISOVdmgilDJQoTz1sEm34ida98JFjdzSgkUvJ/pFT
+ Z21ThCNxlUf01Hr2Pdcg1e2/97sZocMFTY2JKwmiW2LG3B05/VrRtdvsCVj8G49c
+ oxgPPKty+m71ovAXdS/CvVqE7TefCplsYJ1dV3abwwf/Xl2SxzbAKbrYMgZfdGzp
+ Pg2u6982WvfGIVfjFJh9veHZAbfkPcjdAD2Xe67Y4BeKG2OQQqeOY2y+81A7Paeh
+ gHzbFHJG/4RjqB66efrZAg4DgIdbr4oyMoUJVVsl0wfYSIvnd4kvWXYICVwk53HL
+ A3wIowZAsJ1LT2byAKbUzayLzTekrTzGcwQkg2XT798ev2QuR5Ki5x8MULBFX4Lh
+ d03+uGOxjhNPQD6DAAHCQLaXQhaGuyMgt5hDt0nF3yuw3Eg4Ygcbtm24rZXOHJc9
+ bDKeRiWZz1dIyYYVJmHSQwOVXkAwJlF1sIgy/jQYeOgFDKq20x86WulkvsUtBoaZ
+ Jg==
+ =TKlF
+ -----END PGP PRIVATE KEY BLOCK-----
+ SECRET
+ end
+
+ def fingerprint
+ 'EA3F8B88972A8FD474872F2F115B3AF1AAD3EDBA'
+ end
+
+ def subkey_fingerprints
+ %w(159AD5DDF199591D67D2B87AA3CEC5C0A7C270EC 0522DD29B98F167CD8421752E38FFCAF75ABD92A)
+ end
+
+ def names
+ ['John Doe']
+ end
+
+ def emails
+ ['john.doe@example.com']
+ end
+ end
+
+ # GPG Key containing just the main key
+ module User4
+ extend self
+
+ def public_key
+ <<~KEY.strip
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+ mQENBFnWcesBCAC6Y8FXl9ZJ9HPa6dIYcgQrvjIQcwoQCUEsaXNRpc+206RPCIXK
+ aIYr0nTD8GeovMuUONXTj+DdueQU2GAAqHHOqvDDVXqRrW3xfWnSwix7sTuhG1Ew
+ PLHYmjLENqaTsdyliEo3N8VWy2k0QRbC3R6xvop4Ooa87D5vcATIl0gYFtSiHIL+
+ TervYvTG9Eq1qSLZHbe2x4IzeqX2luikPKokL7j8FTZaCmC5MezIUur1ulfyYY/j
+ SkST/1aUFc5QXJJSZA0MYJWZX6x7Y3l7yl0dkHqmK8OTuo8RPWd3ybEiuvRsOL8K
+ GAv/PmVJRGDAf7GGbwXXsE9MiZ5GzVPxHnexABEBAAG0G0pvaG4gRG9lIDxqb2hu
+ QGV4YW1wbGUuY29tPokBTgQTAQgAOBYhBAh0izYM0lwuzJnVlAcBbPnhOj+bBQJZ
+ 1nHrAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEAcBbPnhOj+bkywH/i4w
+ OwpDxoTjUQlPlqGAGuzvWaPzSJndawgmMTr68oRsD+wlQmQQTR5eqxCpUIyV4aYb
+ D697RYzoqbT4mlU49ymzfKSAxFe88r1XQWdm81DcofHVPmw2GBrIqaX3Du4Z7xkI
+ Q9/S43orwknh5FoVwU8Nau7qBuv9vbw2apSkuA1oBj3spQ8hqwLavACyQ+fQloAT
+ hSDNqPiCZj6L0dwM1HYiqVoN3Q7qjgzzeBzlXzljJoWblhxllvMK20bVoa7H+uR2
+ lczFHfsX8VTIMjyTGP7R3oHN91DEahlQybVVNLmNSDKZM2P/0d28BRUmWxQJ4Ws3
+ J4hOWDKnLMed3VOIWzM=
+ =xVuW
+ -----END PGP PUBLIC KEY BLOCK-----
+ KEY
+ end
+
+ def secret_key
+ <<~KEY.strip
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+ lQPGBFnWcesBCAC6Y8FXl9ZJ9HPa6dIYcgQrvjIQcwoQCUEsaXNRpc+206RPCIXK
+ aIYr0nTD8GeovMuUONXTj+DdueQU2GAAqHHOqvDDVXqRrW3xfWnSwix7sTuhG1Ew
+ PLHYmjLENqaTsdyliEo3N8VWy2k0QRbC3R6xvop4Ooa87D5vcATIl0gYFtSiHIL+
+ TervYvTG9Eq1qSLZHbe2x4IzeqX2luikPKokL7j8FTZaCmC5MezIUur1ulfyYY/j
+ SkST/1aUFc5QXJJSZA0MYJWZX6x7Y3l7yl0dkHqmK8OTuo8RPWd3ybEiuvRsOL8K
+ GAv/PmVJRGDAf7GGbwXXsE9MiZ5GzVPxHnexABEBAAH+BwMC4UwgHgH5Cp7meY39
+ G5Q3GV2xtwADoaAvlOvPOLPK2fQqxQfb4WN4eZECp2wQuMRBMj52c4i9yphab1mQ
+ vOzoPIRGvkcJoxG++OxQ0kRk0C0gX6wM6SGVdb1nQnfZnoJCCU3IwCaSGktkLDs1
+ jwdI+VmXJbSugUbd25bakHQcE2BaNHuRBlQWQfFbhGBy0+uMfNDBZ6FRipBu47hO
+ f/wm/xXuV8N8BSgvNR/qtAqSQI34CdsnWAhMYm9rqmTNyt0nq4dveX+E0YzVn4lH
+ lOEa7cpYeuBwIL8L3EvSPNCICiJlF3gVqiYzyqRElnCkv1OGc0x3W5onY/agHgGZ
+ KYyi/ubOdqqDgBR+eMt0JKSGH2EPxUAGFPY5F37u4erdxH86GzIinAExLSmADiVR
+ KtxluZP6S2KLbETN5uVbrfa+HVcMbbUZaBHHtL+YbY8PqaFUIvIUR1HM2SK7IrFw
+ KuQ8ibRgooyP7VgMNiPzlFpY4NXUv+FXIrNJ6ELuIaENi0izJ7aIbVBM8SijDz6u
+ 5EEmodnDvmU2hmQNZJ17TxggE7oeT0rKdDGHM5zBvqZ3deqE9sgKx/aTKcj61ID3
+ M80ZkHPDFazUCohLpYgFN20bYYSmxU4LeNFy8YEiuic8QQKaAFxSf9Lf87UFQwyF
+ dduI1RWEbjMsbEJXwlmGM02ssQHsgoVKwZxijq5A5R1Ul6LowazQ8obPiwRS4NZ4
+ Z+QKDon79MMXiFEeh1jeG/MKKWPxFg3pdtCWhC7WdH4hfkBsCVKf+T58yB2Gzziy
+ fOHvAl7v3PtdZgf1xikF8spGYGCWo4B2lxC79xIflKAb2U6myb5I4dpUYxzxoMxT
+ zxHwxEie3NxzZGUyXSt3LqYe2r4CxWnOCXWjIxxRlLue1BE5Za1ycnDRjgUO24+Z
+ uDQne6KLkhAotBtKb2huIERvZSA8am9obkBleGFtcGxlLmNvbT6JAU4EEwEIADgW
+ IQQIdIs2DNJcLsyZ1ZQHAWz54To/mwUCWdZx6wIbAwULCQgHAgYVCAkKCwIEFgID
+ AQIeAQIXgAAKCRAHAWz54To/m5MsB/4uMDsKQ8aE41EJT5ahgBrs71mj80iZ3WsI
+ JjE6+vKEbA/sJUJkEE0eXqsQqVCMleGmGw+ve0WM6Km0+JpVOPcps3ykgMRXvPK9
+ V0FnZvNQ3KHx1T5sNhgayKml9w7uGe8ZCEPf0uN6K8JJ4eRaFcFPDWru6gbr/b28
+ NmqUpLgNaAY97KUPIasC2rwAskPn0JaAE4Ugzaj4gmY+i9HcDNR2IqlaDd0O6o4M
+ 83gc5V85YyaFm5YcZZbzCttG1aGux/rkdpXMxR37F/FUyDI8kxj+0d6BzfdQxGoZ
+ UMm1VTS5jUgymTNj/9HdvAUVJlsUCeFrNyeITlgypyzHnd1TiFsz
+ =/37z
+ -----END PGP PRIVATE KEY BLOCK-----
+ KEY
+ end
+
+ def primary_keyid
+ fingerprint[-16..-1]
+ end
+
+ def fingerprint
+ '08748B360CD25C2ECC99D59407016CF9E13A3F9B'
+ end
+ end
end
diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb
new file mode 100644
index 00000000000..fd22e384b1b
--- /dev/null
+++ b/spec/support/helpers/merge_request_diff_helpers.rb
@@ -0,0 +1,28 @@
+module MergeRequestDiffHelpers
+ def click_diff_line(line_holder, diff_side = nil)
+ line = get_line_components(line_holder, diff_side)
+ line[:content].hover
+ line[:num].find('.add-diff-note').trigger('click')
+ end
+
+ def get_line_components(line_holder, diff_side = nil)
+ if diff_side.nil?
+ get_inline_line_components(line_holder)
+ else
+ get_parallel_line_components(line_holder, diff_side)
+ end
+ end
+
+ def get_inline_line_components(line_holder)
+ { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
+ end
+
+ def get_parallel_line_components(line_holder, diff_side = nil)
+ side_index = diff_side == 'left' ? 0 : 1
+ # Wait for `.line_content`
+ line_holder.find('.line_content', match: :first)
+ # Wait for `.diff-line-num`
+ line_holder.find('.diff-line-num', match: :first)
+ { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
+ end
+end
diff --git a/spec/support/ldap_shared_examples.rb b/spec/support/ldap_shared_examples.rb
new file mode 100644
index 00000000000..52c34e78965
--- /dev/null
+++ b/spec/support/ldap_shared_examples.rb
@@ -0,0 +1,69 @@
+shared_examples_for 'normalizes a DN' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:test_description, :given, :expected) do
+ 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com'
+ 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith'
+ 'unescapes non-reserved, non-special Unicode characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith, ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebastián c. smith,ou=people (aka. \\"humans\\"),dc=example,dc=com'
+ 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com'
+ 'for a null DN (empty string), returns empty string and does not error' | '' | ''
+ 'does not strip an escaped leading space in an attribute value' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com'
+ 'does not strip an escaped leading space in the last attribute value' | 'uid=\\ John Smith' | 'uid=\\ john smith'
+ 'does not strip an escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com'
+ 'strips extraneous spaces after an escaped trailing space' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com'
+ 'strips extraneous spaces after an escaped trailing space at the end of the DN' | 'uid=John Smith,ou=People,dc=example,dc=com\\ ' | 'uid=john smith,ou=people,dc=example,dc=com\\ '
+ 'properly preserves escaped trailing space after unescaped trailing spaces' | 'uid=John Smith \\ ,ou=People,dc=example,dc=com' | 'uid=john smith \\ ,ou=people,dc=example,dc=com'
+ 'preserves multiple inner spaces in an attribute value' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com'
+ 'preserves inner spaces after an escaped space' | 'uid=John\\ Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com'
+ 'hex-escapes an escaped leading newline in an attribute value' | "uid=\\\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com"
+ 'hex-escapes and does not strip an escaped trailing newline in an attribute value' | "uid=John Smith\\\n,ou=People,dc=example,dc=com" | "uid=john smith\\0a,ou=people,dc=example,dc=com"
+ 'hex-escapes an unescaped leading newline (actually an invalid DN?)' | "uid=\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com"
+ 'strips an unescaped trailing newline (actually an invalid DN?)' | "uid=John Smith\n,ou=People,dc=example,dc=com" | "uid=john smith,ou=people,dc=example,dc=com"
+ 'does not strip if no extraneous whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com'
+ 'does not modify an escaped equal sign in an attribute value' | 'uid= foo \\= bar' | 'uid=foo \\= bar'
+ 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | 'uid= foo \\3D bar' | 'uid=foo \\= bar'
+ 'does not modify an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca'
+ 'converts an escaped hex comma to an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\, ca'
+ 'does not modify an escaped hex carriage return character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0DCA' | 'uid=john c. smith,ou=san francisco\\,\\0dca'
+ 'does not modify an escaped hex line feed character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0aca'
+ 'does not modify an escaped hex CRLF in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0D\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0d\\0aca'
+ 'allows attribute type name OIDs' | '0.9.2342.19200300.100.1.25=Example,0.9.2342.19200300.100.1.25=Com' | '0.9.2342.19200300.100.1.25=example,0.9.2342.19200300.100.1.25=com'
+ 'strips extraneous whitespace from attribute type name OIDs' | '0.9.2342.19200300.100.1.25 = Example, 0.9.2342.19200300.100.1.25 = Com' | '0.9.2342.19200300.100.1.25=example,0.9.2342.19200300.100.1.25=com'
+ end
+
+ with_them do
+ it 'normalizes the DN' do
+ assert_generic_test(test_description, subject, expected)
+ end
+ end
+end
+
+shared_examples_for 'normalizes a DN attribute value' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:test_description, :given, :expected) do
+ 'strips extraneous whitespace' | ' John Smith ' | 'john smith'
+ 'unescapes non-reserved, non-special Unicode characters' | 'Sebasti\\c3\\a1n\\ C.\\20Smith' | 'sebastián c. smith'
+ 'downcases the whole string' | 'JoHn C. Smith' | 'john c. smith'
+ 'does not strip an escaped leading space in an attribute value' | '\\ John Smith' | '\\ john smith'
+ 'does not strip an escaped trailing space in an attribute value' | 'John Smith\\ ' | 'john smith\\ '
+ 'hex-escapes an escaped leading newline in an attribute value' | "\\\nJohn Smith" | "\\0ajohn smith"
+ 'hex-escapes and does not strip an escaped trailing newline in an attribute value' | "John Smith\\\n" | "john smith\\0a"
+ 'hex-escapes an unescaped leading newline (actually an invalid DN value?)' | "\nJohn Smith" | "\\0ajohn smith"
+ 'strips an unescaped trailing newline (actually an invalid DN value?)' | "John Smith\n" | "john smith"
+ 'does not strip if no extraneous whitespace' | 'John Smith' | 'john smith'
+ 'does not modify an escaped equal sign in an attribute value' | ' foo \\= bar' | 'foo \\= bar'
+ 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | ' foo \\3D bar' | 'foo \\= bar'
+ 'does not modify an escaped comma in an attribute value' | 'San Francisco\\, CA' | 'san francisco\\, ca'
+ 'converts an escaped hex comma to an escaped comma in an attribute value' | 'San Francisco\\2C CA' | 'san francisco\\, ca'
+ 'does not modify an escaped hex carriage return character in an attribute value' | 'San Francisco\\,\\0DCA' | 'san francisco\\,\\0dca'
+ 'does not modify an escaped hex line feed character in an attribute value' | 'San Francisco\\,\\0ACA' | 'san francisco\\,\\0aca'
+ 'does not modify an escaped hex CRLF in an attribute value' | 'San Francisco\\,\\0D\\0ACA' | 'san francisco\\,\\0d\\0aca'
+ end
+
+ with_them do
+ it 'normalizes the DN attribute value' do
+ assert_generic_test(test_description, subject, expected)
+ end
+ end
+end
diff --git a/spec/support/project_forks_helper.rb b/spec/support/project_forks_helper.rb
new file mode 100644
index 00000000000..0d1c6792d13
--- /dev/null
+++ b/spec/support/project_forks_helper.rb
@@ -0,0 +1,58 @@
+module ProjectForksHelper
+ def fork_project(project, user = nil, params = {})
+ # Load the `fork_network` for the project to fork as there might be one that
+ # wasn't loaded yet.
+ project.reload unless project.fork_network
+
+ unless user
+ user = create(:user)
+ project.add_developer(user)
+ end
+
+ unless params[:namespace] || params[:namespace_id]
+ params[:namespace] = create(:group)
+ params[:namespace].add_owner(user)
+ end
+
+ service = Projects::ForkService.new(project, user, params)
+
+ create_repository = params.delete(:repository)
+ # Avoid creating a repository
+ unless create_repository
+ allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
+ shell = double('gitlab_shell', fork_repository: true)
+ allow(service).to receive(:gitlab_shell).and_return(shell)
+ end
+
+ forked_project = service.execute
+
+ # Reload the both projects so they know about their newly created fork_network
+ if forked_project.persisted?
+ project.reload
+ forked_project.reload
+ end
+
+ if create_repository
+ # The call to project.repository.after_import in RepositoryForkWorker does
+ # not reset the @exists variable of this forked_project.repository
+ # so we have to explicitely call this method to clear the @exists variable.
+ # of the instance we're returning here.
+ forked_project.repository.after_import
+
+ # We can't leave the hooks in place after a fork, as those would fail in tests
+ # The "internal" API is not available
+ FileUtils.rm_rf("#{forked_project.repository.path}/hooks")
+ end
+
+ forked_project
+ end
+
+ def fork_project_with_submodules(project, user = nil, params = {})
+ forked_project = fork_project(project, user, params)
+ TestEnv.copy_repo(forked_project,
+ bare_repo: TestEnv.forked_repo_path_bare,
+ refs: TestEnv::FORKED_BRANCH_SHA)
+
+ forked_project
+ end
+end
diff --git a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
new file mode 100644
index 00000000000..221926aaf7e
--- /dev/null
+++ b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
@@ -0,0 +1,28 @@
+shared_examples 'comment on merge request file' do
+ it 'adds a comment' do
+ click_diff_line(find("[id='#{sample_commit.line_code}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in(:note_note, with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.notes_holder') do
+ expect(page).to have_content('Line is wrong')
+ end
+
+ visit(merge_request_path(merge_request))
+
+ page.within('.notes .discussion') do
+ expect(page).to have_content("#{user.name} #{user.to_reference} started a discussion")
+ expect(page).to have_content(sample_commit.line_code_path)
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('1')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
new file mode 100644
index 00000000000..a4762b68858
--- /dev/null
+++ b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
@@ -0,0 +1,57 @@
+# This shared example requires a `builder` and `user` variable
+shared_examples 'issuable hook data' do |kind|
+ let(:data) { builder.build(user: user) }
+
+ include_examples 'project hook data' do
+ let(:project) { builder.issuable.project }
+ end
+ include_examples 'deprecated repository hook data'
+
+ context "with a #{kind}" do
+ it 'contains issuable data' do
+ expect(data[:object_kind]).to eq(kind)
+ expect(data[:user]).to eq(user.hook_attrs)
+ expect(data[:project]).to eq(builder.issuable.project.hook_attrs)
+ expect(data[:object_attributes]).to eq(builder.issuable.hook_attrs)
+ expect(data[:changes]).to eq({})
+ expect(data[:repository]).to eq(builder.issuable.project.hook_attrs.slice(:name, :url, :description, :homepage))
+ end
+
+ it 'does not contain certain keys' do
+ expect(data).not_to have_key(:assignees)
+ expect(data).not_to have_key(:assignee)
+ end
+
+ describe 'changes are given' do
+ let(:changes) do
+ {
+ cached_markdown_version: %w[foo bar],
+ description: ['A description', 'A cool description'],
+ description_html: %w[foo bar],
+ in_progress_merge_commit_sha: %w[foo bar],
+ lock_version: %w[foo bar],
+ merge_jid: %w[foo bar],
+ title: ['A title', 'Hello World'],
+ title_html: %w[foo bar]
+ }
+ end
+ let(:data) { builder.build(user: user, changes: changes) }
+
+ it 'populates the :changes hash' do
+ expect(data[:changes]).to match(hash_including({
+ title: { previous: 'A title', current: 'Hello World' },
+ description: { previous: 'A description', current: 'A cool description' }
+ }))
+ end
+
+ it 'does not contain certain keys' do
+ expect(data[:changes]).not_to have_key('cached_markdown_version')
+ expect(data[:changes]).not_to have_key('description_html')
+ expect(data[:changes]).not_to have_key('lock_version')
+ expect(data[:changes]).not_to have_key('title_html')
+ expect(data[:changes]).not_to have_key('in_progress_merge_commit_sha')
+ expect(data[:changes]).not_to have_key('merge_jid')
+ end
+ end
+ end
+end
diff --git a/spec/support/project_hook_data_shared_example.rb b/spec/support/shared_examples/models/project_hook_data_shared_examples.rb
index 1eb405d4be8..f0264878811 100644
--- a/spec/support/project_hook_data_shared_example.rb
+++ b/spec/support/shared_examples/models/project_hook_data_shared_examples.rb
@@ -1,4 +1,4 @@
-RSpec.shared_examples 'project hook data with deprecateds' do |project_key: :project|
+shared_examples 'project hook data with deprecateds' do |project_key: :project|
it 'contains project data' do
expect(data[project_key][:name]).to eq(project.name)
expect(data[project_key][:description]).to eq(project.description)
@@ -17,7 +17,7 @@ RSpec.shared_examples 'project hook data with deprecateds' do |project_key: :pro
end
end
-RSpec.shared_examples 'project hook data' do |project_key: :project|
+shared_examples 'project hook data' do |project_key: :project|
it 'contains project data' do
expect(data[project_key][:name]).to eq(project.name)
expect(data[project_key][:description]).to eq(project.description)
@@ -32,7 +32,7 @@ RSpec.shared_examples 'project hook data' do |project_key: :project|
end
end
-RSpec.shared_examples 'deprecated repository hook data' do |project_key: :project|
+shared_examples 'deprecated repository hook data' do
it 'contains deprecated repository data' do
expect(data[:repository][:name]).to eq(project.name)
expect(data[:repository][:description]).to eq(project.description)
diff --git a/spec/support/shared_examples/position_formatters.rb b/spec/support/shared_examples/position_formatters.rb
new file mode 100644
index 00000000000..ffc9456dbc7
--- /dev/null
+++ b/spec/support/shared_examples/position_formatters.rb
@@ -0,0 +1,43 @@
+shared_examples_for "position formatter" do
+ let(:formatter) { described_class.new(attrs) }
+
+ describe '#key' do
+ let(:key) { [123, 456, 789, Digest::SHA1.hexdigest(formatter.old_path), Digest::SHA1.hexdigest(formatter.new_path), 1, 2] }
+
+ subject { formatter.key }
+
+ it { is_expected.to eq(key) }
+ end
+
+ describe '#complete?' do
+ subject { formatter.complete? }
+
+ context 'when there are missing key attributes' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when old_line and new_line are nil' do
+ let(:attrs) { base_attrs }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#to_h' do
+ let(:formatter_hash) do
+ attrs.merge(position_type: base_attrs[:position_type] || 'text' )
+ end
+
+ subject { formatter.to_h }
+
+ it { is_expected.to eq(formatter_hash) }
+ end
+
+ describe '#==' do
+ subject { formatter }
+
+ let(:other_formatter) { described_class.new(attrs) }
+
+ it { is_expected.to eq(other_formatter) }
+ end
+end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 79395f4c564..a27bfdee3d2 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -46,7 +46,8 @@ module TestEnv
'v1.1.0' => 'b83d6e3',
'add-ipython-files' => '93ee732',
'add-pdf-file' => 'e774ebd',
- 'add-pdf-text-binary' => '79faa7b'
+ 'add-pdf-text-binary' => '79faa7b',
+ 'add_images_and_changes' => '010d106'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 0c8c8a2ab05..886052d7848 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -224,17 +224,20 @@ describe 'gitlab:app namespace rake task' do
end
context 'multiple repository storages' do
+ let(:gitaly_address) { Gitlab.config.repositories.storages.default.gitaly_address }
+ let(:storages) do
+ {
+ 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address },
+ 'test_second_storage' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
+ }
+ end
+
let(:project_a) { create(:project, :repository, repository_storage: 'default') }
- let(:project_b) { create(:project, :repository, repository_storage: 'custom') }
+ let(:project_b) { create(:project, :repository, repository_storage: 'test_second_storage') }
before do
FileUtils.mkdir('tmp/tests/default_storage')
FileUtils.mkdir('tmp/tests/custom_storage')
- gitaly_address = Gitlab.config.repositories.storages.default.gitaly_address
- storages = {
- 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address },
- 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
- }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
# Create the projects now, after mocking the settings but before doing the backup
@@ -253,7 +256,7 @@ describe 'gitlab:app namespace rake task' do
after do
FileUtils.rm_rf('tmp/tests/default_storage')
FileUtils.rm_rf('tmp/tests/custom_storage')
- FileUtils.rm(@backup_tar)
+ FileUtils.rm(@backup_tar) if @backup_tar
end
it 'includes repositories in all repository storages' do
diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
index 98c7de9b709..efed2e02a1b 100644
--- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
describe 'projects/merge_requests/_commits.html.haml' do
include Devise::Test::ControllerHelpers
+ include ProjectForksHelper
let(:user) { create(:user) }
- let(:target_project) { create(:project, :repository) }
- let(:source_project) { create(:project, :repository, forked_from_project: target_project) }
+ let(:target_project) { create(:project, :public, :repository) }
+ let(:source_project) { fork_project(target_project, user, repository: true) }
let(:merge_request) do
create(:merge_request, :simple,
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
index 69c7d0cbf28..9b74a7e1946 100644
--- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -2,16 +2,19 @@ require 'spec_helper'
describe 'projects/merge_requests/edit.html.haml' do
include Devise::Test::ControllerHelpers
+ include ProjectForksHelper
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:project, :repository, forked_from_project: project) }
- let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
let(:milestone) { create(:milestone, project: project) }
let(:closed_merge_request) do
+ project.add_developer(user)
+
create(:closed_merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project,
author: user,
assignee: user,
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index 6f29d12373a..28d54c2fb77 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -2,16 +2,17 @@ require 'spec_helper'
describe 'projects/merge_requests/show.html.haml' do
include Devise::Test::ControllerHelpers
+ include ProjectForksHelper
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:project, :repository, forked_from_project: project) }
- let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) }
let(:closed_merge_request) do
create(:closed_merge_request,
- source_project: fork_project,
+ source_project: forked_project,
target_project: project,
author: user)
end
@@ -52,7 +53,7 @@ describe 'projects/merge_requests/show.html.haml' do
context 'when the merge request is open' do
it 'closes the merge request if the source project does not exist' do
closed_merge_request.update_attributes(state: 'open')
- fork_project.destroy
+ forked_project.destroy
render
diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb
index 8cc3f37ebe8..1a7ffd5cdbf 100644
--- a/spec/workers/build_finished_worker_spec.rb
+++ b/spec/workers/build_finished_worker_spec.rb
@@ -11,6 +11,8 @@ describe BuildFinishedWorker do
expect(BuildHooksWorker)
.to receive(:new).ordered.and_call_original
+ expect(BuildTraceSectionsWorker)
+ .to receive(:perform_async)
expect_any_instance_of(BuildCoverageWorker)
.to receive(:perform)
expect_any_instance_of(BuildHooksWorker)
diff --git a/spec/workers/build_trace_sections_worker_spec.rb b/spec/workers/build_trace_sections_worker_spec.rb
new file mode 100644
index 00000000000..45243f45547
--- /dev/null
+++ b/spec/workers/build_trace_sections_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe BuildTraceSectionsWorker do
+ describe '#perform' do
+ context 'when build exists' do
+ let!(:build) { create(:ci_build) }
+
+ it 'updates trace sections' do
+ expect_any_instance_of(Ci::Build)
+ .to receive(:parse_trace_sections!)
+
+ described_class.new.perform(build.id)
+ end
+ end
+
+ context 'when build does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb
new file mode 100644
index 00000000000..11f208289db
--- /dev/null
+++ b/spec/workers/cluster_provision_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe ClusterProvisionWorker do
+ describe '#perform' do
+ context 'when cluster exists' do
+ let(:cluster) { create(:gcp_cluster) }
+
+ it 'provision a cluster' do
+ expect_any_instance_of(Ci::ProvisionClusterService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when cluster does not exist' do
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Ci::ProvisionClusterService).not_to receive(:execute)
+
+ described_class.new.perform(123)
+ end
+ end
+ end
+end
diff --git a/spec/workers/concerns/cluster_queue_spec.rb b/spec/workers/concerns/cluster_queue_spec.rb
new file mode 100644
index 00000000000..1050651fa51
--- /dev/null
+++ b/spec/workers/concerns/cluster_queue_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe ClusterQueue do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ include ClusterQueue
+ end
+ end
+
+ it 'sets a default pipelines queue automatically' do
+ expect(worker.sidekiq_options['queue'])
+ .to eq :gcp_cluster
+ end
+end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index ab656d619f4..47297de738b 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -104,7 +104,7 @@ describe GitGarbageCollectWorker do
it_should_behave_like 'flushing ref caches', true
end
- context "with Gitaly turned off", skip_gitaly_mock: true do
+ context "with Gitaly turned off", :skip_gitaly_mock do
it_should_behave_like 'flushing ref caches', false
end
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
index 20cf580af8a..ed8cedc0079 100644
--- a/spec/workers/namespaceless_project_destroy_worker_spec.rb
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe NamespacelessProjectDestroyWorker do
+ include ProjectForksHelper
+
subject { described_class.new }
before do
@@ -55,9 +57,11 @@ describe NamespacelessProjectDestroyWorker do
context 'project forked from another' do
let!(:parent_project) { create(:project) }
-
- before do
- create(:forked_project_link, forked_to_project: project, forked_from_project: parent_project)
+ let(:project) do
+ namespaceless_project = fork_project(parent_project)
+ namespaceless_project.namespace_id = nil
+ namespaceless_project.save(validate: false)
+ namespaceless_project
end
it 'closes open merge requests' do
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index d9e9409840f..e881ec37ae5 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -12,6 +12,28 @@ describe RepositoryForkWorker do
end
describe "#perform" do
+ describe 'when a worker was reset without cleanup' do
+ let(:jid) { '12345678' }
+ let(:started_project) { create(:project, :repository, :import_started) }
+
+ it 'creates a new repository from a fork' do
+ allow(subject).to receive(:jid).and_return(jid)
+
+ expect(shell).to receive(:fork_repository).with(
+ '/test/path',
+ project.full_path,
+ project.repository_storage_path,
+ fork_project.namespace.full_path
+ ).and_return(true)
+
+ subject.perform(
+ project.id,
+ '/test/path',
+ project.full_path,
+ fork_project.namespace.full_path)
+ end
+ end
+
it "creates a new repository from a fork" do
expect(shell).to receive(:fork_repository).with(
'/test/path',
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 100dfc32bbe..5cff5108477 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -6,6 +6,23 @@ describe RepositoryImportWorker do
subject { described_class.new }
describe '#perform' do
+ context 'when worker was reset without cleanup' do
+ let(:jid) { '12345678' }
+ let(:started_project) { create(:project, :import_started, import_jid: jid) }
+
+ it 'imports the project successfully' do
+ allow(subject).to receive(:jid).and_return(jid)
+
+ expect_any_instance_of(Projects::ImportService).to receive(:execute)
+ .and_return({ status: :ok })
+
+ expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
+ expect_any_instance_of(Project).to receive(:import_finish)
+
+ subject.perform(project.id)
+ end
+ end
+
context 'when the import was successful' do
it 'imports a project' do
expect_any_instance_of(Projects::ImportService).to receive(:execute)
diff --git a/spec/workers/wait_for_cluster_creation_worker_spec.rb b/spec/workers/wait_for_cluster_creation_worker_spec.rb
new file mode 100644
index 00000000000..dcd4a3b9aec
--- /dev/null
+++ b/spec/workers/wait_for_cluster_creation_worker_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe WaitForClusterCreationWorker do
+ describe '#perform' do
+ context 'when cluster exists' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:operation) { double }
+
+ before do
+ allow(operation).to receive(:status).and_return(status)
+ allow(operation).to receive(:start_time).and_return(1.minute.ago)
+ allow(operation).to receive(:status_message).and_return('error')
+ allow_any_instance_of(Ci::FetchGcpOperationService).to receive(:execute).and_yield(operation)
+ end
+
+ context 'when operation status is RUNNING' do
+ let(:status) { 'RUNNING' }
+
+ it 'reschedules worker' do
+ expect(described_class).to receive(:perform_in)
+
+ described_class.new.perform(cluster.id)
+ end
+
+ context 'when operation timeout' do
+ before do
+ allow(operation).to receive(:start_time).and_return(30.minutes.ago.utc)
+ end
+
+ it 'sets an error message on cluster' do
+ described_class.new.perform(cluster.id)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+
+ context 'when operation status is DONE' do
+ let(:status) { 'DONE' }
+
+ it 'finalizes cluster creation' do
+ expect_any_instance_of(Ci::FinalizeClusterCreationService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when operation status is others' do
+ let(:status) { 'others' }
+
+ it 'sets an error message on cluster' do
+ described_class.new.perform(cluster.id)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+
+ context 'when cluster does not exist' do
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Ci::FetchGcpOperationService).not_to receive(:execute)
+
+ described_class.new.perform(1234)
+ end
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index ae86887630b..57644482b32 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2713,8 +2713,8 @@ getpass@^0.1.1:
assert-plus "^1.0.0"
"gitlab-svgs@https://gitlab.com/gitlab-org/gitlab-svgs.git":
- version "1.0.2"
- resolved "https://gitlab.com/gitlab-org/gitlab-svgs.git#e7621d7b028607ae9c69f8b496cd49b42fe607e4"
+ version "1.0.4"
+ resolved "https://gitlab.com/gitlab-org/gitlab-svgs.git#46c0a49cd43639948dfcc77a0f94d59deaad1e85"
glob-base@^0.3.0:
version "0.3.0"