summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClement Ho <clemmakesapps@gmail.com>2018-06-26 22:15:08 +0000
committerClement Ho <clemmakesapps@gmail.com>2018-06-26 22:15:08 +0000
commit6ebc75f8ac61049d81aef00d916244c5b17c0568 (patch)
treeb72a2ea7533e0653e7d93ee4cc55299533b96971
parent65ba27da4990ad45ebbd45f66be70d6a09824e72 (diff)
parent292cf668905a55e7b305c67b314cb039d2681a54 (diff)
downloadgitlab-ce-redesign-mr-widget-info-pipeline.tar.gz
[skip ci] Merge branch 'master' into 'redesign-mr-widget-info-pipeline'redesign-mr-widget-info-pipeline
# Conflicts: # app/assets/stylesheets/pages/merge_requests.scss
-rw-r--r--.eslintrc.yml2
-rw-r--r--.gitignore2
-rw-r--r--.gitlab-ci.yml51
-rw-r--r--.nvmrc2
-rw-r--r--CHANGELOG.md300
-rw-r--r--CONTRIBUTING.md4
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock14
-rw-r--r--Gemfile.rails5.lock12
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/ajax_loading_spinner.js2
-rw-r--r--app/assets/javascripts/autosave.js4
-rw-r--r--app/assets/javascripts/awards_handler.js191
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js4
-rw-r--r--app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js2
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js2
-rw-r--r--app/assets/javascripts/blob/stl_viewer.js2
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js16
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue (renamed from app/assets/javascripts/boards/components/modal/empty_state.js)62
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue (renamed from app/assets/javascripts/boards/components/modal/footer.js)56
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js8
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js54
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.vue56
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js46
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue49
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js73
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue72
-rw-r--r--app/assets/javascripts/boards/index.js4
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js2
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js2
-rw-r--r--app/assets/javascripts/commit/image_file.js4
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js2
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js2
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js2
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js61
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js32
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js40
-rw-r--r--app/assets/javascripts/diffs/components/app.vue199
-rw-r--r--app/assets/javascripts/diffs/components/changed_files.vue184
-rw-r--r--app/assets/javascripts/diffs/components/changed_files_dropdown.vue124
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue55
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue165
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue62
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue39
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue191
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue254
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue105
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue203
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue114
-rw-r--r--app/assets/javascripts/diffs/components/edit_button.vue42
-rw-r--r--app/assets/javascripts/diffs/components/hidden_files_warning.vue51
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue117
-rw-r--r--app/assets/javascripts/diffs/components/no_changes.vue49
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue224
-rw-r--r--app/assets/javascripts/diffs/constants.js25
-rw-r--r--app/assets/javascripts/diffs/index.js41
-rw-r--r--app/assets/javascripts/diffs/mixins/changed_files.js38
-rw-r--r--app/assets/javascripts/diffs/mixins/diff_content.js89
-rw-r--r--app/assets/javascripts/diffs/store/actions.js100
-rw-r--r--app/assets/javascripts/diffs/store/getters.js16
-rw-r--r--app/assets/javascripts/diffs/store/index.js11
-rw-r--r--app/assets/javascripts/diffs/store/modules/index.js26
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js11
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js86
-rw-r--r--app/assets/javascripts/diffs/store/utils.js172
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue2
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js6
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js4
-rw-r--r--app/assets/javascripts/filtered_search/recent_searches_root.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js23
-rw-r--r--app/assets/javascripts/gl_dropdown.js11
-rw-r--r--app/assets/javascripts/gl_form.js15
-rw-r--r--app/assets/javascripts/groups/index.js4
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue6
-rw-r--r--app/assets/javascripts/ide/components/file_finder/item.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue1
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue39
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue2
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff_worker.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js13
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js5
-rw-r--r--app/assets/javascripts/image_diff/helpers/dom_helper.js3
-rw-r--r--app/assets/javascripts/image_diff/helpers/utils_helper.js3
-rw-r--r--app/assets/javascripts/init_notes.js4
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js2
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js2
-rw-r--r--app/assets/javascripts/labels_select.js9
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js150
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js7
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js21
-rw-r--r--app/assets/javascripts/line_highlighter.js8
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js4
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js2
-rw-r--r--app/assets/javascripts/merge_request.js1
-rw-r--r--app/assets/javascripts/merge_request_tabs.js15
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js4
-rw-r--r--app/assets/javascripts/mr_notes/index.js51
-rw-r--r--app/assets/javascripts/mr_notes/stores/actions.js7
-rw-r--r--app/assets/javascripts/mr_notes/stores/getters.js5
-rw-r--r--app/assets/javascripts/mr_notes/stores/index.js15
-rw-r--r--app/assets/javascripts/mr_notes/stores/modules/index.js12
-rw-r--r--app/assets/javascripts/mr_notes/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/mr_notes/stores/mutations.js7
-rw-r--r--app/assets/javascripts/network/branch_graph.js55
-rw-r--r--app/assets/javascripts/new_branch_form.js2
-rw-r--r--app/assets/javascripts/notes.js268
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue51
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue125
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue19
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue20
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue13
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue55
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue11
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue207
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue51
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue84
-rw-r--r--app/assets/javascripts/notes/constants.js2
-rw-r--r--app/assets/javascripts/notes/index.js81
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js6
-rw-r--r--app/assets/javascripts/notes/mixins/noteable.js9
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js36
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js8
-rw-r--r--app/assets/javascripts/notes/stores/actions.js41
-rw-r--r--app/assets/javascripts/notes/stores/getters.js71
-rw-r--r--app/assets/javascripts/notes/stores/index.js26
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js26
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js4
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js96
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue2
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js9
-rw-r--r--app/assets/javascripts/pages/projects/init_form.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/form.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js17
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/tags/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js5
-rw-r--r--app/assets/javascripts/pages/snippets/form.js9
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js2
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js2
-rw-r--r--app/assets/javascripts/preview_markdown.js2
-rw-r--r--app/assets/javascripts/profile/gl_crop.js21
-rw-r--r--app/assets/javascripts/project_find_file.js5
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
-rw-r--r--app/assets/javascripts/projects_dropdown/index.js2
-rw-r--r--app/assets/javascripts/registry/index.js2
-rw-r--r--app/assets/javascripts/registry/stores/actions.js2
-rw-r--r--app/assets/javascripts/shared/milestones/form.js10
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js24
-rw-r--r--app/assets/javascripts/smart_interval.js9
-rw-r--r--app/assets/javascripts/test_utils/simulate_drag.js4
-rw-r--r--app/assets/javascripts/u2f/authenticate.js8
-rw-r--r--app/assets/javascripts/users_select.js11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue141
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.vue2
-rw-r--r--app/assets/stylesheets/bootstrap.scss37
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss8
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/animations.scss9
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss6
-rw-r--r--app/assets/stylesheets/highlight/dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss4
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss4
-rw-r--r--app/assets/stylesheets/pages/boards.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss25
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss5
-rw-r--r--app/assets/stylesheets/pages/note_form.scss16
-rw-r--r--app/assets/stylesheets/pages/notes.scss70
-rw-r--r--app/assets/stylesheets/pages/repo.scss42
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb4
-rw-r--r--app/controllers/admin/users_controller.rb4
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/notes_actions.rb6
-rw-r--r--app/controllers/health_controller.rb2
-rw-r--r--app/controllers/metrics_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb4
-rw-r--r--app/controllers/projects/blob_controller.rb44
-rw-r--r--app/controllers/projects/commits_controller.rb14
-rw-r--r--app/controllers/projects/discussions_controller.rb9
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb25
-rw-r--r--app/controllers/projects/merge_requests_controller.rb3
-rw-r--r--app/controllers/projects/wikis_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb31
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/finders/user_recent_events_finder.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/notes_helper.rb15
-rw-r--r--app/helpers/projects_helper.rb11
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/models/application_setting.rb13
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/sortable.rb4
-rw-r--r--app/models/discussion.rb4
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/label.rb15
-rw-r--r--app/models/merge_request.rb36
-rw-r--r--app/models/merge_request_diff.rb27
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/note.rb1
-rw-r--r--app/models/project.rb4
-rw-r--r--app/models/project_auto_devops.rb4
-rw-r--r--app/models/project_team.rb2
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/presenters/merge_request_presenter.rb11
-rw-r--r--app/serializers/blob_entity.rb4
-rw-r--r--app/serializers/diff_file_entity.rb123
-rw-r--r--app/serializers/diffs_entity.rb65
-rw-r--r--app/serializers/diffs_serializer.rb3
-rw-r--r--app/serializers/discussion_entity.rb49
-rw-r--r--app/serializers/merge_request_diff_entity.rb46
-rw-r--r--app/serializers/merge_request_user_entity.rb24
-rw-r--r--app/serializers/merge_request_widget_entity.rb12
-rw-r--r--app/serializers/note_entity.rb28
-rw-r--r--app/services/base_count_service.rb6
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb1
-rw-r--r--app/services/merge_requests/delete_non_latest_diffs_service.rb18
-rw-r--r--app/services/merge_requests/merge_request_diff_cache_service.rb17
-rw-r--r--app/services/merge_requests/post_merge_service.rb5
-rw-r--r--app/services/merge_requests/reload_diffs_service.rb43
-rw-r--r--app/services/projects/count_service.rb6
-rw-r--r--app/services/projects/open_issues_count_service.rb32
-rw-r--r--app/services/web_hook_service.rb2
-rw-r--r--app/views/admin/application_settings/show.html.haml3
-rw-r--r--app/views/admin/labels/_form.html.haml10
-rw-r--r--app/views/admin/labels/_label.html.haml4
-rw-r--r--app/views/admin/labels/edit.html.haml8
-rw-r--r--app/views/admin/labels/index.html.haml8
-rw-r--r--app/views/admin/labels/new.html.haml4
-rw-r--r--app/views/devise/sessions/_new_base.html.haml4
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml4
-rw-r--r--app/views/groups/new.html.haml46
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml5
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml6
-rw-r--r--app/views/notify/merge_request_unmergeable_email.html.haml6
-rw-r--r--app/views/notify/merge_request_unmergeable_email.text.haml5
-rw-r--r--app/views/profiles/keys/_form.html.haml6
-rw-r--r--app/views/profiles/keys/index.html.haml9
-rw-r--r--app/views/projects/clusters/_gcp_signup_offer_banner.html.haml2
-rw-r--r--app/views/projects/clusters/_integration_form.html.haml35
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml8
-rw-r--r--app/views/projects/deployments/_rollback.haml4
-rw-r--r--app/views/projects/graphs/charts.html.haml2
-rw-r--r--app/views/projects/issues/_discussion.html.haml3
-rw-r--r--app/views/projects/merge_requests/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml36
-rw-r--r--app/views/shared/boards/_show.html.haml4
-rw-r--r--app/views/shared/boards/components/_board.html.haml6
-rw-r--r--app/views/shared/boards/components/sidebar/_due_date.html.haml12
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml8
-rw-r--r--app/views/shared/boards/components/sidebar/_milestone.html.haml12
-rw-r--r--app/views/shared/notes/_form.html.haml2
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml7
-rw-r--r--app/views/shared/runners/show.html.haml14
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml1
-rw-r--r--app/views/u2f/_authenticate.html.haml3
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/delete_diff_files_worker.rb17
-rwxr-xr-xbin/changelog49
-rw-r--r--changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml5
-rw-r--r--changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml5
-rw-r--r--changelogs/unreleased/22647-width-contributors-graphs.yml5
-rw-r--r--changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml6
-rw-r--r--changelogs/unreleased/23465-print-markdown.yml5
-rw-r--r--changelogs/unreleased/25045-add-variables-to-post-pipeline-api.yml5
-rw-r--r--changelogs/unreleased/25955-update-404-pages.yml5
-rw-r--r--changelogs/unreleased/36862-subgroup-milestones.yml5
-rw-r--r--changelogs/unreleased/38542-application-control-panel-in-settings-page.yml5
-rw-r--r--changelogs/unreleased/38759-fetch-available-parameters-directly-from-gke-when-creating-a-cluster.yml5
-rw-r--r--changelogs/unreleased/38919-wiki-empty-states.yml5
-rw-r--r--changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml5
-rw-r--r--changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml5
-rw-r--r--changelogs/unreleased/39710-search-placeholder-cut-off.yml5
-rw-r--r--changelogs/unreleased/40005-u2f-unspported-browsers.yml5
-rw-r--r--changelogs/unreleased/40725-move-mr-external-link-to-right.yml5
-rw-r--r--changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml5
-rw-r--r--changelogs/unreleased/41587-osw-mr-metrics-migration-cleanup.yml5
-rw-r--r--changelogs/unreleased/42531-open-invite-404.yml5
-rw-r--r--changelogs/unreleased/42751-rename-master-to-maintainer.yml5
-rw-r--r--changelogs/unreleased/42751-rename-mr-maintainer-push.yml5
-rw-r--r--changelogs/unreleased/43367-fix-board-long-strings.yml5
-rw-r--r--changelogs/unreleased/43597-new-navigation-themes.yml5
-rw-r--r--changelogs/unreleased/43673-operations-tab-mvc.yml5
-rw-r--r--changelogs/unreleased/44184-issues_ical_feed.yml5
-rw-r--r--changelogs/unreleased/44267-improve-failed-jobs-tab.yml5
-rw-r--r--changelogs/unreleased/44319-remove-gray-buttons.yml5
-rw-r--r--changelogs/unreleased/44579-ide-add-pipeline-to-status-bar.yml5
-rw-r--r--changelogs/unreleased/44790-disabled-emails-logging.yml5
-rw-r--r--changelogs/unreleased/44799-api-naming-issue-scope.yml5
-rw-r--r--changelogs/unreleased/45065-users-projects-json-sort.yml5
-rw-r--r--changelogs/unreleased/45190-create-notes-diff-files.yml5
-rw-r--r--changelogs/unreleased/45404-remove-gemnasium-badge-from-project-s-readme-md.yml5
-rw-r--r--changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml5
-rw-r--r--changelogs/unreleased/45505-lograge_formatter_encoding.yml6
-rw-r--r--changelogs/unreleased/45520-remove-links-from-web-ide.yml5
-rw-r--r--changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml5
-rw-r--r--changelogs/unreleased/45702-fix-hashed-storage-repository-archive.yml5
-rw-r--r--changelogs/unreleased/45703-open-web-ide-file-tree.yml5
-rw-r--r--changelogs/unreleased/45715-remove-modal-retry.yml5
-rw-r--r--changelogs/unreleased/45820-add-xcode-link.yml5
-rw-r--r--changelogs/unreleased/45821-avatar_api.yml5
-rw-r--r--changelogs/unreleased/45827-expose_readme_url_in_project_api.yml5
-rw-r--r--changelogs/unreleased/45850-close-mr-checkout-modal-on-escape.yml5
-rw-r--r--changelogs/unreleased/45933-webide-fade-uneditable-area.yml5
-rw-r--r--changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml5
-rw-r--r--changelogs/unreleased/46010-add-index-to-runner-type.yml5
-rw-r--r--changelogs/unreleased/46019-add-missing-migration.yml5
-rw-r--r--changelogs/unreleased/46075-automatically-provide-deploy-token-when-autodevops-is-enabled.yml5
-rw-r--r--changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml5
-rw-r--r--changelogs/unreleased/46193-fix-big-estimate.yml5
-rw-r--r--changelogs/unreleased/46202-webide-file-states.yml5
-rw-r--r--changelogs/unreleased/46354-deprecate_gemnasium_service.yml5
-rw-r--r--changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml5
-rw-r--r--changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key-part-1.yml5
-rw-r--r--changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml5
-rw-r--r--changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml5
-rw-r--r--changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml5
-rw-r--r--changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml5
-rw-r--r--changelogs/unreleased/46452-nomethoderror-undefined-method-previous_changes-for-nil-nilclass.yml5
-rw-r--r--changelogs/unreleased/46478-update-updated-at-on-mr.yml5
-rw-r--r--changelogs/unreleased/46487-add-support-for-jupyter-in-gitlab-via-kubernetes.yml5
-rw-r--r--changelogs/unreleased/46552-fixes-redundant-message-for-failure-reasons.yml5
-rw-r--r--changelogs/unreleased/46571-webhooks-nil-password.yml5
-rw-r--r--changelogs/unreleased/46585-gdpr-terms-acceptance.yml6
-rw-r--r--changelogs/unreleased/46648-timeout-searching-group-issues.yml5
-rw-r--r--changelogs/unreleased/46783-removed-omniauth-provider-causing-invalid-application-setting.yml5
-rw-r--r--changelogs/unreleased/46831-remove-unused-bootstrap-component-css.yml5
-rw-r--r--changelogs/unreleased/46844-update-awesome_print-to-1-8-0.yml5
-rw-r--r--changelogs/unreleased/46845-update-email_spec-to-2-2-0.yml5
-rw-r--r--changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml5
-rw-r--r--changelogs/unreleased/46849-update-rdoc-to-6-0-4.yml5
-rw-r--r--changelogs/unreleased/46903-osw-fix-permitted-params-filtering-on-merge-scheduling.yml5
-rw-r--r--changelogs/unreleased/46922-hashed-storage-single-project.yml5
-rw-r--r--changelogs/unreleased/46999-line-profiling-modal-width.yml5
-rw-r--r--changelogs/unreleased/47046-use-sortable-from-npm.yml5
-rw-r--r--changelogs/unreleased/47113-modal-header-styling-is-broken.yml5
-rw-r--r--changelogs/unreleased/47182-use-the-default-strings-of-timeago-js.yml5
-rw-r--r--changelogs/unreleased/47183-update-selenium-webdriver-to-3-12-0.yml5
-rw-r--r--changelogs/unreleased/47189-github_import_visibility.yml6
-rw-r--r--changelogs/unreleased/47208-human-import-status-name-not-working.yml5
-rw-r--r--changelogs/unreleased/47221-explain-what-groups-are-in-the-new-group-page.yml5
-rw-r--r--changelogs/unreleased/47274-help-users-find-our-contributing-page.yml5
-rw-r--r--changelogs/unreleased/47408-migrateuploadsworker-is-doing-n-1-queries-on-migration.yml5
-rw-r--r--changelogs/unreleased/47513-upload-migration-lease-key-is-incorrect-for-non-mounted-uploaders.yml5
-rw-r--r--changelogs/unreleased/47604-avatars-and-system-icons-for-mobile.yml5
-rw-r--r--changelogs/unreleased/47646-ui-glitch.yml5
-rw-r--r--changelogs/unreleased/47672-set_inline_content_type_for_ics.yml5
-rw-r--r--changelogs/unreleased/47679-fix-failed-jobs-tab-ie11.yml5
-rw-r--r--changelogs/unreleased/47794-environment-scope-cluster-page.yml6
-rw-r--r--changelogs/unreleased/47871-new-mr-tab-active-state.yml5
-rw-r--r--changelogs/unreleased/48126-fix-prometheus-installation.yml5
-rw-r--r--changelogs/unreleased/48378-avatar-upload.yml5
-rw-r--r--changelogs/unreleased/ab-35364-throttle-updates-last-repository-at.yml5
-rw-r--r--changelogs/unreleased/ab-43706-composite-primary-keys.yml5
-rw-r--r--changelogs/unreleased/ab-45389-remove-double-checked-internal-id-generation.yml5
-rw-r--r--changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml5
-rw-r--r--changelogs/unreleased/add-artifacts_expire_at-to-api.yml5
-rw-r--r--changelogs/unreleased/add-background-migration-to-fill-file-store.yml5
-rw-r--r--changelogs/unreleased/add-background-migrations-for-not-archived-traces.yml5
-rw-r--r--changelogs/unreleased/add-missing-index-for-deployments.yml5
-rw-r--r--changelogs/unreleased/add-moneky-patch-for-using-stream-upload-with-carrierwave.yml5
-rw-r--r--changelogs/unreleased/add-new-arg-to-git-rev-list-call.yml5
-rw-r--r--changelogs/unreleased/bjk-48176_ruby_gc.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-fix-protect-from-forgery-in-application-controller.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-fix-data-store-spec.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-set-request-format-in--commits-controller.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-remove-spinach.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-squash-and-merge-in-gitlab-core-ce.yml5
-rw-r--r--changelogs/unreleased/bootstrap-changelog.yml5
-rw-r--r--changelogs/unreleased/bump-kubeclient-version-3-1-0.yml5
-rw-r--r--changelogs/unreleased/bvl-add-username-to-terms-message.yml5
-rw-r--r--changelogs/unreleased/bvl-bump-gitlab-shell-7-1-3.yml5
-rw-r--r--changelogs/unreleased/bvl-graphql-start-34754.yml5
-rw-r--r--changelogs/unreleased/bvl-terms-on-registration.yml5
-rw-r--r--changelogs/unreleased/ccr-incoming-email-regex-anchor.yml3
-rw-r--r--changelogs/unreleased/collapsed-contextual-nav-update.yml6
-rw-r--r--changelogs/unreleased/commit-branch-tag-icon-update.yml5
-rw-r--r--changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml5
-rw-r--r--changelogs/unreleased/dm-api-projects-members-preload.yml6
-rw-r--r--changelogs/unreleased/dm-label-reference-period.yml5
-rw-r--r--changelogs/unreleased/docs-42067-document-runner-registration-api.yml5
-rw-r--r--changelogs/unreleased/dz-add-2fa-filter.yml5
-rw-r--r--changelogs/unreleased/dz-redesign-group-settings-page.yml5
-rw-r--r--changelogs/unreleased/existing-gcp-accounts.yml5
-rw-r--r--changelogs/unreleased/feature-customizable-favicon.yml5
-rw-r--r--changelogs/unreleased/feature-expose-runner-ip-to-api.yml5
-rw-r--r--changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml5
-rw-r--r--changelogs/unreleased/fix-assignee-name-wrap.yml5
-rw-r--r--changelogs/unreleased/fix-avatars-n-plus-one.yml5
-rw-r--r--changelogs/unreleased/fix-bitbucket_import_anonymous.yml5
-rw-r--r--changelogs/unreleased/fix-boards-issue-highlight.yml5
-rw-r--r--changelogs/unreleased/fix-devops-remove-beta.yml5
-rw-r--r--changelogs/unreleased/fix-favicon-cross-origin.yml5
-rw-r--r--changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml5
-rw-r--r--changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml5
-rw-r--r--changelogs/unreleased/fix-http-proxy.yml5
-rw-r--r--changelogs/unreleased/fix-missing-timeout.yml5
-rw-r--r--changelogs/unreleased/fix-nbsp-after-sign-in-with-google.yml5
-rw-r--r--changelogs/unreleased/fix-reactive-cache-retry-rate.yml5
-rw-r--r--changelogs/unreleased/fix-registry-created-at-tooltip.yml5
-rw-r--r--changelogs/unreleased/fix-shorcut-modal.yml5
-rw-r--r--changelogs/unreleased/fix-unverified-hover-state.yml5
-rw-r--r--changelogs/unreleased/fj-34526-enabling-wiki-search-by-title.yml5
-rw-r--r--changelogs/unreleased/fj-36819-remove-v3-api.yml5
-rw-r--r--changelogs/unreleased/fj-40401-support-import-lfs-objects.yml5
-rw-r--r--changelogs/unreleased/fj-45059-add-validation-to-webhook.yml5
-rw-r--r--changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml5
-rw-r--r--changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml5
-rw-r--r--changelogs/unreleased/fj-relax-url-validator-rules-for-user.yml5
-rw-r--r--changelogs/unreleased/fj-restore-users-v3-endpoint.yml5
-rw-r--r--changelogs/unreleased/gh-importer-transactions.yml5
-rw-r--r--changelogs/unreleased/groups-controller-show-performance.yml5
-rw-r--r--changelogs/unreleased/ide-hide-merge-request-if-disabled.yml5
-rw-r--r--changelogs/unreleased/ide-url-util-relative-url-fix.yml6
-rw-r--r--changelogs/unreleased/ignore-writing-trace-if-it-already-archived.yml5
-rw-r--r--changelogs/unreleased/introduce-job-keep-alive-api-endpoint.yml5
-rw-r--r--changelogs/unreleased/issue-25256.yml5
-rw-r--r--changelogs/unreleased/issue_38418.yml5
-rw-r--r--changelogs/unreleased/issue_44230.yml5
-rw-r--r--changelogs/unreleased/issue_45082.yml5
-rw-r--r--changelogs/unreleased/issue_47729.yml5
-rw-r--r--changelogs/unreleased/jivl-add-dot-system-notes.yml5
-rw-r--r--changelogs/unreleased/jivl-smarter-system-notes.yml5
-rw-r--r--changelogs/unreleased/jprovazn-fix-resolvable.yml5
-rw-r--r--changelogs/unreleased/jprovazn-null-byte.yml5
-rw-r--r--changelogs/unreleased/jprovazn-pipeline-policy.yml6
-rw-r--r--changelogs/unreleased/jprovazn-remote-upload-destroy.yml5
-rw-r--r--changelogs/unreleased/jprovazn-uploader-migration.yml5
-rw-r--r--changelogs/unreleased/jr-48133-web-ide-commit-ellipsis.yml5
-rw-r--r--changelogs/unreleased/jr-web-ide-shortcuts.yml5
-rw-r--r--changelogs/unreleased/live-trace-v2-persist-data.yml5
-rw-r--r--changelogs/unreleased/mattermost-api-v4.yml5
-rw-r--r--changelogs/unreleased/migrate-restore-repo-to-gitaly.yml5
-rw-r--r--changelogs/unreleased/mk-rake-task-verify-remote-files.yml5
-rw-r--r--changelogs/unreleased/move-boards-modal-empty-state-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-disussion-actions-to-the-right.yml5
-rw-r--r--changelogs/unreleased/mr-conflict-notification.yml5
-rw-r--r--changelogs/unreleased/n-plus-one-notification-recipients.yml5
-rw-r--r--changelogs/unreleased/new-label-spelling-error.yml5
-rw-r--r--changelogs/unreleased/no-multi-assign-enable.yml5
-rw-r--r--changelogs/unreleased/no-multi-assign-follow-up.yml5
-rw-r--r--changelogs/unreleased/no-restricted-globals-enable.yml5
-rw-r--r--changelogs/unreleased/optimise-pages-service-calling.yml5
-rw-r--r--changelogs/unreleased/optimise-paused-runners.yml5
-rw-r--r--changelogs/unreleased/optimise-runner-update-cached-info.yml5
-rw-r--r--changelogs/unreleased/osw-delete-non-latest-mr-diff-files-upon-merge.yml5
-rw-r--r--changelogs/unreleased/osw-ignore-diff-header-when-persisting-diff-hunk.yml5
-rw-r--r--changelogs/unreleased/patch-28.yml5
-rw-r--r--changelogs/unreleased/per-project-pipeline-iid.yml5
-rw-r--r--changelogs/unreleased/pipelines-index-performance.yml5
-rw-r--r--changelogs/unreleased/prefer-destructuring-fix.yml5
-rw-r--r--changelogs/unreleased/presigned-multipart-uploads.yml5
-rw-r--r--changelogs/unreleased/rails5-active-sup-subscriber.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-46230.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-46236.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-46281.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-47368.yml6
-rw-r--r--changelogs/unreleased/rails5-fix-47376.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-48140.yml6
-rw-r--r--changelogs/unreleased/rails5-fix-mysql-arel-from.yml5
-rw-r--r--changelogs/unreleased/rd-44364-deprecate-support-for-dsa-keys.yml5
-rw-r--r--changelogs/unreleased/reactive-caching-alive-bug.yml6
-rw-r--r--changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml5
-rw-r--r--changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml5
-rw-r--r--changelogs/unreleased/remove-unused-query-in-hooks.yml5
-rw-r--r--changelogs/unreleased/rename-merge-request-widget-author-component.yml5
-rw-r--r--changelogs/unreleased/revert-merge-request-widget-button-height.yml5
-rw-r--r--changelogs/unreleased/security-2682-fix-xss-for-markdown-toc.yml5
-rw-r--r--changelogs/unreleased/security-dm-delete-deploy-key.yml5
-rw-r--r--changelogs/unreleased/security-fj-bumping-sanitize-gem.yml5
-rw-r--r--changelogs/unreleased/security-fj-import-export-assignment.yml5
-rw-r--r--changelogs/unreleased/security-html_escape_branch_name.yml5
-rw-r--r--changelogs/unreleased/security-html_escape_usernames.yml5
-rw-r--r--changelogs/unreleased/security-rd-do-not-show-internal-info-in-public-feed.yml5
-rw-r--r--changelogs/unreleased/security-users-can-update-their-password-without-entering-current-password.yml5
-rw-r--r--changelogs/unreleased/sh-add-uncached-query-limiter.yml5
-rw-r--r--changelogs/unreleased/sh-batch-dependent-destroys.yml5
-rw-r--r--changelogs/unreleased/sh-bump-omniauth-gitlab.yml5
-rw-r--r--changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml5
-rw-r--r--changelogs/unreleased/sh-expire-content-cache-after-import.yml5
-rw-r--r--changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml5
-rw-r--r--changelogs/unreleased/sh-fix-backup-specific-rake-task.yml5
-rw-r--r--changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml5
-rw-r--r--changelogs/unreleased/sh-fix-events-nplus-one.yml5
-rw-r--r--changelogs/unreleased/sh-fix-grape-logging-status-code.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-api-perf-n-plus-one.yml5
-rw-r--r--changelogs/unreleased/sh-fix-pipeline-jobs-nplus-one.yml5
-rw-r--r--changelogs/unreleased/sh-fix-secrets-not-working.yml5
-rw-r--r--changelogs/unreleased/sh-fix-source-project-nplus-one.yml5
-rw-r--r--changelogs/unreleased/sh-improve-import-status-error.yml5
-rw-r--r--changelogs/unreleased/sh-log-422-responses.yml6
-rw-r--r--changelogs/unreleased/sh-move-delete-groups-api-async.yml5
-rw-r--r--changelogs/unreleased/sh-tag-queue-duration-api-calls.yml5
-rw-r--r--changelogs/unreleased/sh-use-grape-path-helpers.yml5
-rw-r--r--changelogs/unreleased/sql-buckets.yml5
-rw-r--r--changelogs/unreleased/support-active-setting-while-registering-a-runner.yml5
-rw-r--r--changelogs/unreleased/update-external-link-icon-in-header-user-dropdown.yml5
-rw-r--r--changelogs/unreleased/update-help-integration-screenshot.yml5
-rw-r--r--changelogs/unreleased/update-pipeline-icon-in-web-ide-sidebar.yml5
-rw-r--r--changelogs/unreleased/update-wiki-modal.yml5
-rw-r--r--changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml5
-rw-r--r--changelogs/unreleased/winh-make-it-right-now.yml5
-rw-r--r--changelogs/unreleased/zj-add-branch-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-calculate-checksum-mandator.yml5
-rw-r--r--changelogs/unreleased/zj-ref-contains-sha-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-wiki-find-file-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-workhorse-archive-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-workhorse-commit-patch-diff.yml5
-rw-r--r--config/application.rb19
-rw-r--r--config/initializers/active_record_data_types.rb2
-rw-r--r--config/initializers/devise.rb4
-rw-r--r--config/karma.config.js1
-rw-r--r--config/locales/doorkeeper.en.yml16
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--config/webpack.config.js6
-rw-r--r--db/migrate/20180626125654_add_index_on_deployable_for_deployments.rb18
-rw-r--r--db/migrate/merge_request_diff_file_limits_to_mysql.rb2
-rw-r--r--db/schema.rb3
-rw-r--r--doc/administration/job_traces.md2
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md14
-rw-r--r--doc/api/runners.md12
-rw-r--r--doc/development/documentation/index.md39
-rw-r--r--doc/development/documentation/styleguide.md18
-rw-r--r--doc/development/fe_guide/vuex.md4
-rw-r--r--doc/development/gotchas.md48
-rw-r--r--doc/development/i18n/proofreader.md2
-rw-r--r--doc/development/query_recorder.md1
-rw-r--r--doc/development/utilities.md41
-rw-r--r--doc/development/what_requires_downtime.md33
-rw-r--r--doc/install/installation.md5
-rw-r--r--doc/install/kubernetes/gitlab_chart.md137
-rw-r--r--doc/install/kubernetes/index.md51
-rw-r--r--doc/install/kubernetes/preparation/connect.md31
-rw-r--r--doc/install/kubernetes/preparation/eks.md44
-rw-r--r--doc/install/kubernetes/preparation/networking.md36
-rw-r--r--doc/install/kubernetes/preparation/rbac.md16
-rw-r--r--doc/install/kubernetes/preparation/tiller.md94
-rw-r--r--doc/install/kubernetes/preparation/tools_installation.md19
-rw-r--r--doc/integration/recaptcha.md19
-rw-r--r--doc/integration/saml.md75
-rw-r--r--doc/topics/autodevops/img/auto_monitoring.pngbin69473 -> 26675 bytes
-rw-r--r--doc/topics/autodevops/img/guide_choose_gke.pngbin0 -> 7895 bytes
-rw-r--r--doc/topics/autodevops/img/guide_cluster_apps.pngbin0 -> 28667 bytes
-rw-r--r--doc/topics/autodevops/img/guide_connect_cluster.pngbin38724 -> 15225 bytes
-rw-r--r--doc/topics/autodevops/img/guide_create_cluster.pngbin0 -> 18915 bytes
-rw-r--r--doc/topics/autodevops/img/guide_create_project.pngbin0 -> 17704 bytes
-rw-r--r--doc/topics/autodevops/img/guide_enable_autodevops.pngbin0 -> 27763 bytes
-rw-r--r--doc/topics/autodevops/img/guide_environments.pngbin0 -> 8570 bytes
-rw-r--r--doc/topics/autodevops/img/guide_environments_metrics.pngbin0 -> 10231 bytes
-rw-r--r--doc/topics/autodevops/img/guide_first_pipeline.pngbin0 -> 10350 bytes
-rw-r--r--doc/topics/autodevops/img/guide_gitlab_gke_details.pngbin0 -> 22677 bytes
-rw-r--r--doc/topics/autodevops/img/guide_gke_apis_after.pngbin0 -> 26811 bytes
-rw-r--r--doc/topics/autodevops/img/guide_gke_apis_before.pngbin0 -> 14882 bytes
-rw-r--r--doc/topics/autodevops/img/guide_google_auth.pngbin0 -> 12729 bytes
-rw-r--r--doc/topics/autodevops/img/guide_google_signin.pngbin0 -> 14343 bytes
-rw-r--r--doc/topics/autodevops/img/guide_ide_commit.pngbin0 -> 22035 bytes
-rw-r--r--doc/topics/autodevops/img/guide_integration.pngbin44263 -> 0 bytes
-rw-r--r--doc/topics/autodevops/img/guide_merge_request.pngbin0 -> 31157 bytes
-rw-r--r--doc/topics/autodevops/img/guide_merge_request_ide.pngbin0 -> 35052 bytes
-rw-r--r--doc/topics/autodevops/img/guide_merge_request_review_app.pngbin0 -> 25596 bytes
-rw-r--r--doc/topics/autodevops/img/guide_pipeline_stages.pngbin0 -> 12557 bytes
-rw-r--r--doc/topics/autodevops/img/guide_project_landing_page.pngbin0 -> 19227 bytes
-rw-r--r--doc/topics/autodevops/img/guide_project_template.pngbin0 -> 14699 bytes
-rw-r--r--doc/topics/autodevops/img/guide_secret.pngbin16233 -> 0 bytes
-rw-r--r--doc/topics/autodevops/img/rollout_staging_disabled.pngbin13837 -> 13834 bytes
-rw-r--r--doc/topics/autodevops/img/rollout_staging_enabled.pngbin17306 -> 17299 bytes
-rw-r--r--doc/topics/autodevops/img/staging_enabled.pngbin17929 -> 17922 bytes
-rw-r--r--doc/topics/autodevops/index.md43
-rw-r--r--doc/topics/autodevops/quick_start_guide.md347
-rw-r--r--doc/update/10.8-to-11.0.md7
-rw-r--r--doc/user/admin_area/settings/sign_up_restrictions.md1
-rw-r--r--doc/user/project/clusters/index.md89
-rw-r--r--doc/user/project/img/group_issue_board.pngbin0 -> 163417 bytes
-rw-r--r--doc/user/project/img/issue_board.pngbin82592 -> 100684 bytes
-rw-r--r--doc/user/project/img/issue_board_add_list.pngbin17312 -> 6404 bytes
-rw-r--r--doc/user/project/img/issue_board_assignee_lists.pngbin0 -> 134674 bytes
-rw-r--r--doc/user/project/img/issue_board_creation.pngbin0 -> 108674 bytes
-rw-r--r--doc/user/project/img/issue_board_edit_button.pngbin0 -> 108168 bytes
-rw-r--r--doc/user/project/img/issue_board_focus_mode.gifbin0 -> 1043366 bytes
-rw-r--r--doc/user/project/img/issue_board_move_issue_card_list.pngbin36747 -> 13670 bytes
-rw-r--r--doc/user/project/img/issue_board_system_notes.pngbin4899 -> 4893 bytes
-rw-r--r--doc/user/project/img/issue_board_view_scope.pngbin0 -> 63542 bytes
-rw-r--r--doc/user/project/img/issue_board_welcome_message.pngbin26533 -> 13519 bytes
-rw-r--r--doc/user/project/img/issue_boards_add_issues_modal.pngbin29176 -> 12421 bytes
-rw-r--r--doc/user/project/img/issue_boards_multiple.pngbin0 -> 6092 bytes
-rw-r--r--doc/user/project/img/issue_boards_remove_issue.pngbin135168 -> 39357 bytes
-rw-r--r--doc/user/project/import/bitbucket.md4
-rw-r--r--doc/user/project/issue_board.md212
-rw-r--r--doc/user/project/issues/deleting_issues.md4
-rw-r--r--doc/user/project/merge_requests/squash_and_merge.md6
-rw-r--r--doc/workflow/lfs/lfs_administration.md95
-rw-r--r--doc/workflow/notifications.md2
-rw-r--r--doc/workflow/todos.md2
-rw-r--r--lib/api/branches.rb8
-rw-r--r--lib/backup/repository.rb10
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb6
-rw-r--r--lib/banzai/filter/sanitization_filter.rb3
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb2
-rw-r--r--lib/gitlab/auth/o_auth/user.rb4
-rw-r--r--lib/gitlab/auth/saml/auth_hash.rb15
-rw-r--r--lib/gitlab/auth/saml/config.rb4
-rw-r--r--lib/gitlab/auth/saml/user.rb4
-rw-r--r--lib/gitlab/background_migration/cleanup_concurrent_rename.rb14
-rw-r--r--lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb52
-rw-r--r--lib/gitlab/background_migration/cleanup_concurrent_type_change.rb48
-rw-r--r--lib/gitlab/cache/request_cache.rb37
-rw-r--r--lib/gitlab/ci/variables/collection/item.rb3
-rw-r--r--lib/gitlab/database/median.rb8
-rw-r--r--lib/gitlab/database/migration_helpers.rb91
-rw-r--r--lib/gitlab/diff/file.rb31
-rw-r--r--lib/gitlab/diff/line.rb33
-rw-r--r--lib/gitlab/diff/parser.rb8
-rw-r--r--lib/gitlab/git/blob.rb113
-rw-r--r--lib/gitlab/git/commit.rb10
-rw-r--r--lib/gitlab/git/gitlab_projects.rb25
-rw-r--r--lib/gitlab/git/remote_mirror.rb77
-rw-r--r--lib/gitlab/git/repository.rb165
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb26
-rw-r--r--lib/gitlab/request_forgery_protection.rb2
-rw-r--r--lib/gitlab/usage_data.rb16
-rw-r--r--lib/gitlab/verify/uploads.rb2
-rw-r--r--lib/mysql_zero_date.rb18
-rw-r--r--lib/tasks/flay.rake2
-rw-r--r--lib/tasks/gettext.rake3
-rw-r--r--locale/gitlab.pot151
-rw-r--r--package.json3
-rw-r--r--qa/qa.rb13
-rw-r--r--qa/qa/factory/repository/project_push.rb34
-rw-r--r--qa/qa/factory/repository/push.rb22
-rw-r--r--qa/qa/factory/repository/wiki_push.rb32
-rw-r--r--qa/qa/factory/resource/branch.rb4
-rw-r--r--qa/qa/factory/resource/merge_request.rb4
-rw-r--r--qa/qa/factory/resource/wiki.rb25
-rw-r--r--qa/qa/page/menu/side.rb7
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb4
-rw-r--r--qa/qa/page/project/show.rb41
-rw-r--r--qa/qa/page/project/wiki/edit.rb27
-rw-r--r--qa/qa/page/project/wiki/new.rb45
-rw-r--r--qa/qa/page/project/wiki/show.rb19
-rw-r--r--qa/qa/page/shared/clone_panel.rb50
-rw-r--r--qa/qa/runtime/env.rb6
-rw-r--r--qa/qa/specs/features/login/ldap_spec.rb6
-rw-r--r--qa/qa/specs/features/merge_request/rebase_spec.rb2
-rw-r--r--qa/qa/specs/features/merge_request/squash_spec.rb2
-rw-r--r--qa/qa/specs/features/project/activity_spec.rb2
-rw-r--r--qa/qa/specs/features/project/auto_devops_spec.rb2
-rw-r--r--qa/qa/specs/features/project/deploy_key_clone_spec.rb2
-rw-r--r--qa/qa/specs/features/project/pipelines_spec.rb2
-rw-r--r--qa/qa/specs/features/project/wikis_spec.rb45
-rw-r--r--qa/qa/specs/features/repository/protected_branches_spec.rb26
-rw-r--r--qa/qa/specs/features/repository/push_spec.rb2
-rw-r--r--rubocop/cop/gitlab/finder_with_find_by.rb52
-rw-r--r--rubocop/cop/migration/update_large_table.rb15
-rw-r--r--rubocop/rubocop.rb1
-rw-r--r--scripts/frontend/postinstall.js22
-rwxr-xr-xscripts/trigger-build4
-rwxr-xr-xscripts/trigger-build-docs2
-rw-r--r--spec/bin/changelog_spec.rb11
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb189
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb60
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb23
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb29
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb14
-rw-r--r--spec/controllers/projects_controller_spec.rb2
-rw-r--r--spec/controllers/sessions_controller_spec.rb39
-rw-r--r--spec/dependencies/omniauth_saml_spec.rb22
-rw-r--r--spec/features/admin/admin_groups_spec.rb1
-rw-r--r--spec/features/admin/admin_settings_spec.rb23
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb2
-rw-r--r--spec/features/issuables/shortcuts_issuable_spec.rb23
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb14
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb22
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb36
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb6
-rw-r--r--spec/features/merge_request/user_locks_discussion_spec.rb4
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb27
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb24
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb21
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb44
-rw-r--r--spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb2
-rw-r--r--spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb10
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb6
-rw-r--r--spec/features/merge_request/user_sees_diff_spec.rb15
-rw-r--r--spec/features/merge_request/user_sees_discussions_spec.rb8
-rw-r--r--spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb3
-rw-r--r--spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_system_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb6
-rw-r--r--spec/features/merge_request/user_uses_slash_commands_spec.rb33
-rw-r--r--spec/features/participants_autocomplete_spec.rb6
-rw-r--r--spec/features/profiles/account_spec.rb2
-rw-r--r--spec/features/projects/commit/comments/user_adds_comment_spec.rb2
-rw-r--r--spec/features/projects/commit/user_comments_on_commit_spec.rb109
-rw-r--r--spec/features/projects/graph_spec.rb20
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_diff_spec.rb8
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb3
-rw-r--r--spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_views_diffs_spec.rb12
-rw-r--r--spec/features/projects/view_on_env_spec.rb4
-rw-r--r--spec/features/users/login_spec.rb35
-rw-r--r--spec/features/users/signup_spec.rb9
-rw-r--r--spec/finders/notes_finder_spec.rb2
-rw-r--r--spec/finders/user_recent_events_finder_spec.rb45
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_widget.json1
-rw-r--r--spec/fixtures/authentication/saml_response.xml42
-rw-r--r--spec/helpers/projects_helper_spec.rb9
-rw-r--r--spec/javascripts/awards_handler_spec.js106
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js81
-rw-r--r--spec/javascripts/blob/3d_viewer/mesh_object_spec.js4
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js2
-rw-r--r--spec/javascripts/deploy_keys/components/key_spec.js2
-rw-r--r--spec/javascripts/diffs/components/app_spec.js1
-rw-r--r--spec/javascripts/diffs/components/changed_files_dropdown_spec.js1
-rw-r--r--spec/javascripts/diffs/components/changed_files_spec.js100
-rw-r--r--spec/javascripts/diffs/components/compare_versions_dropdown_spec.js1
-rw-r--r--spec/javascripts/diffs/components/compare_versions_spec.js1
-rw-r--r--spec/javascripts/diffs/components/diff_content_spec.js95
-rw-r--r--spec/javascripts/diffs/components/diff_discussions_spec.js24
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js433
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js88
-rw-r--r--spec/javascripts/diffs/components/diff_gutter_avatars_spec.js115
-rw-r--r--spec/javascripts/diffs/components/diff_line_gutter_content_spec.js153
-rw-r--r--spec/javascripts/diffs/components/diff_line_note_form_spec.js85
-rw-r--r--spec/javascripts/diffs/components/edit_button_spec.js1
-rw-r--r--spec/javascripts/diffs/components/hidden_files_warning_spec.js1
-rw-r--r--spec/javascripts/diffs/components/inline_diff_view_spec.js111
-rw-r--r--spec/javascripts/diffs/components/no_changes_spec.js1
-rw-r--r--spec/javascripts/diffs/components/parallel_diff_view_spec.js224
-rw-r--r--spec/javascripts/diffs/mock_data/diff_discussions.js496
-rw-r--r--spec/javascripts/diffs/mock_data/diff_file.js220
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js211
-rw-r--r--spec/javascripts/diffs/store/getters_spec.js24
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js149
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js179
-rw-r--r--spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js8
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js11
-rw-r--r--spec/javascripts/filtered_search/recent_searches_root_spec.js3
-rw-r--r--spec/javascripts/fixtures/commit.rb33
-rw-r--r--spec/javascripts/fixtures/snippet.rb1
-rw-r--r--spec/javascripts/gl_field_errors_spec.js2
-rw-r--r--spec/javascripts/groups/components/app_spec.js6
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js2
-rw-r--r--spec/javascripts/helpers/index.js3
-rw-r--r--spec/javascripts/helpers/init_vue_mr_page_helper.js42
-rw-r--r--spec/javascripts/ide/components/repo_tab_spec.js26
-rw-r--r--spec/javascripts/ide/helpers.js26
-rw-r--r--spec/javascripts/ide/lib/diff/controller_spec.js2
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js36
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js70
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js16
-rw-r--r--spec/javascripts/matchers.js26
-rw-r--r--spec/javascripts/merge_request_notes_spec.js108
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js565
-rw-r--r--spec/javascripts/namespace_select_spec.js4
-rw-r--r--spec/javascripts/notebook/cells/markdown_spec.js1
-rw-r--r--spec/javascripts/notes/components/comment_form_spec.js94
-rw-r--r--spec/javascripts/notes/components/diff_file_header_spec.js93
-rw-r--r--spec/javascripts/notes/components/diff_with_note_spec.js20
-rw-r--r--spec/javascripts/notes/components/discussion_counter_spec.js58
-rw-r--r--spec/javascripts/notes/components/note_actions_spec.js12
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js68
-rw-r--r--spec/javascripts/notes/components/note_awards_list_spec.js8
-rw-r--r--spec/javascripts/notes/components/note_body_spec.js7
-rw-r--r--spec/javascripts/notes/components/note_form_spec.js28
-rw-r--r--spec/javascripts/notes/components/note_header_spec.js30
-rw-r--r--spec/javascripts/notes/components/note_signed_out_widget_spec.js20
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js83
-rw-r--r--spec/javascripts/notes/components/noteable_note_spec.js15
-rw-r--r--spec/javascripts/notes/mock_data.js13
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js40
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js20
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js104
-rw-r--r--spec/javascripts/notes_spec.js151
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js29
-rw-r--r--spec/javascripts/pipelines/pipelines_table_spec.js2
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js77
-rw-r--r--spec/javascripts/shortcuts_spec.js19
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js15
-rw-r--r--spec/javascripts/smart_interval_spec.js4
-rw-r--r--spec/javascripts/test_bundle.js6
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js84
-rw-r--r--spec/javascripts/vue_shared/components/file_icon_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/icon_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js20
-rw-r--r--spec/javascripts/vue_shared/components/notes/system_note_spec.js14
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js2
-rw-r--r--spec/javascripts/zen_mode_spec.js59
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb9
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb8
-rw-r--r--spec/lib/gitlab/auth/saml/auth_hash_spec.rb51
-rw-r--r--spec/lib/gitlab/auth/saml/user_spec.rb41
-rw-r--r--spec/lib/gitlab/bitbucket_import/project_creator_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb64
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb12
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb55
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb12
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb160
-rw-r--r--spec/lib/gitlab/gitlab_import/project_creator_spec.rb4
-rw-r--r--spec/lib/gitlab/google_code_import/project_creator_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/legacy_github_import/project_creator_spec.rb5
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb31
-rw-r--r--spec/lib/gitlab/verify/uploads_spec.rb45
-rw-r--r--spec/mailers/notify_spec.rb8
-rw-r--r--spec/models/application_setting_spec.rb36
-rw-r--r--spec/models/ci/build_spec.rb6
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb8
-rw-r--r--spec/models/concerns/sortable_spec.rb18
-rw-r--r--spec/models/merge_request_diff_spec.rb39
-rw-r--r--spec/models/merge_request_spec.rb89
-rw-r--r--spec/models/namespace_spec.rb13
-rw-r--r--spec/models/note_spec.rb10
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb35
-rw-r--r--spec/requests/api/boards_spec.rb1
-rw-r--r--spec/requests/api/branches_spec.rb29
-rw-r--r--spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb56
-rw-r--r--spec/rubocop/cop/migration/update_large_table_spec.rb20
-rw-r--r--spec/serializers/blob_entity_spec.rb20
-rw-r--r--spec/serializers/diff_file_entity_spec.rb48
-rw-r--r--spec/serializers/diffs_entity_spec.rb28
-rw-r--r--spec/serializers/discussion_entity_spec.rb34
-rw-r--r--spec/serializers/merge_request_diff_entity_spec.rb24
-rw-r--r--spec/serializers/merge_request_user_entity_spec.rb19
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb13
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb4
-rw-r--r--spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb59
-rw-r--r--spec/services/merge_requests/merge_request_diff_cache_service_spec.rb39
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb12
-rw-r--r--spec/services/merge_requests/reload_diffs_service_spec.rb64
-rw-r--r--spec/services/projects/batch_open_issues_count_service_spec.rb54
-rw-r--r--spec/services/projects/open_issues_count_service_spec.rb35
-rw-r--r--spec/services/projects/update_remote_mirror_service_spec.rb305
-rw-r--r--spec/services/users/destroy_service_spec.rb8
-rw-r--r--spec/services/web_hook_service_spec.rb30
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/features/reportable_note_shared_examples.rb2
-rw-r--r--spec/support/helpers/expect_next_instance_of.rb13
-rw-r--r--spec/support/helpers/login_helpers.rb36
-rw-r--r--spec/support/helpers/merge_request_diff_helpers.rb2
-rw-r--r--spec/support/helpers/stub_object_storage.rb7
-rw-r--r--spec/support/matchers/match_ids.rb7
-rw-r--r--spec/support/redis/redis_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/serializers/note_entity_examples.rb4
-rw-r--r--spec/uploaders/object_storage_spec.rb12
-rw-r--r--spec/views/devise/shared/_signin_box.html.haml_spec.rb1
-rw-r--r--spec/workers/delete_diff_files_worker_spec.rb41
-rw-r--r--spec/workers/delete_user_worker_spec.rb10
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb10
-rw-r--r--yarn.lock236
892 files changed, 14495 insertions, 5851 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
index b9c5973d7ac..77b1b72fe68 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -69,5 +69,3 @@ rules:
FunctionExpression:
parameters: 1
body: 1
- ## Destructuring: https://eslint.org/docs/rules/prefer-destructuring
- prefer-destructuring: off
diff --git a/.gitignore b/.gitignore
index 21dc67384aa..9a42a663fb4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,7 +29,7 @@ eslint-report.html
/app/assets/javascripts/locale/**/app.js
/backups/*
/config/aws.yml
-/config/database.yml
+/config/database*.yml
/config/gitlab.yml
/config/gitlab_ci.yml
/config/initializers/rack_attack.rb
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 30c21b452e0..8703ef6823a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -220,18 +220,6 @@ stages:
paths:
- log/development.log
-# Review docs base
-.review-docs: &review-docs
- <<: *dedicated-runner
- <<: *except-qa
- <<: *single-script-job
- variables:
- <<: *single-script-job-variables
- SCRIPT_NAME: trigger-build-docs
- when: manual
- only:
- - branches
-
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
@@ -273,20 +261,44 @@ package-and-qa:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
-# Trigger a docs build in gitlab-docs
-# Useful to preview the docs changes live
-review-docs-deploy:
- <<: *review-docs
- stage: build
+# Review docs base
+.review-docs: &review-docs
+ <<: *dedicated-runner
+ <<: *single-script-job
+ variables:
+ <<: *single-script-job-variables
+ SCRIPT_NAME: trigger-build-docs
environment:
name: review-docs/$CI_COMMIT_REF_NAME
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
- url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
+ url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
+
+# Trigger a manual docs build in gitlab-docs only on non docs-only branches.
+# Useful to preview the docs changes live.
+review-docs-deploy-manual:
+ <<: *review-docs
+ stage: build
+ script:
+ - gem install gitlab --no-ri --no-rdoc
+ - ./$SCRIPT_NAME deploy
+ when: manual
+ only:
+ - branches
+ <<: *except-docs-and-qa
+
+# Always trigger a docs build in gitlab-docs only on docs-only branches.
+# Useful to preview the docs changes live.
+review-docs-deploy:
+ <<: *review-docs
+ stage: post-test
script:
- gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME deploy
+ only:
+ - /(^docs[\/-].*|.*-docs$)/
+ <<: *except-qa
# Cleanup remote environment of gitlab-docs
review-docs-cleanup:
@@ -295,9 +307,10 @@ review-docs-cleanup:
environment:
name: review-docs/$CI_COMMIT_REF_NAME
action: stop
+ when: manual
script:
- gem install gitlab --no-ri --no-rdoc
- - ./SCRIPT_NAME cleanup
+ - ./$SCRIPT_NAME cleanup
##
# Trigger a docker image build in CNG (Cloud Native GitLab) repository
diff --git a/.nvmrc b/.nvmrc
index f7ee06693c1..dba04c1e178 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-9.0.0
+8.11.3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d843c3f318..f9f38766392 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,290 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.0.2 (2018-06-26)
+
+### Fixed (8 changes, 1 of them is from the community)
+
+- Serve favicon image always from the main GitLab domain to avoid issues with CORS. !19810 (Alexis Reigel)
+- Specify chart version when installing applications on Clusters. !20010
+- Fix invalid fuzzy translations being generated during installation. !20048
+- Fix incremental rollouts for Auto DevOps. !20061
+- Notify conflict for only open merge request. !20125
+- Only load Omniauth if enabled. !20132
+- Fix sorting by name on explore projects page. !20162
+- Fix alert button styling so that they don't show up white.
+
+### Performance (1 change)
+
+- Remove performance bottleneck preventing large wiki pages from displaying. !20174
+
+### Added (1 change)
+
+- Add support for verifying remote uploads, artifacts, and LFS objects in check rake tasks. !19501
+
+
+## 11.0.1 (2018-06-21)
+
+### Security (5 changes)
+
+- Fix XSS vulnerability for table of content generation.
+- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
+- HTML escape branch name in project graphs page.
+- HTML escape the name of the user in ProjectsHelper#link_to_member.
+- Don't show events from internal projects for anonymous users in public feed.
+
+
+## 11.0.0 (2018-06-22)
+
+### Security (3 changes)
+
+- Fix API to remove deploy key from project instead of deleting it entirely.
+- Fixed bug that allowed importing arbitrary project attributes.
+- Prevent user passwords from being changed without providing the previous password.
+
+### Removed (2 changes)
+
+- Removed API v3 from the codebase. !18970
+- Removes outdated `g t` shortcut for TODO in favor of `Shift+T`. !19002
+
+### Fixed (69 changes, 23 of them are from the community)
+
+- Optimize the upload migration proces. !15947
+- Import bitbucket issues that are reported by an anonymous user. !18199 (bartl)
+- Fix an issue where the notification email address would be set to an unconfirmed email address. !18474
+- Stop logging email information when emails are disabled. !18521 (Marc Shaw)
+- Fix double-brackets being linkified in wiki markdown. !18524 (brewingcode)
+- Use case in-sensitive ordering by name for dashboard. !18553 (@vedharish)
+- Fix width of contributors graphs. !18639 (Paul Vorbach)
+- Fix modal width of shorcuts help page. !18766 (Lars Greiss)
+- Add missing tooltip to creation date on container registry overview. !18767 (Lars Greiss)
+- Add missing migration for minimal Project build_timeout. !18775
+- Update commit status from external CI services less aggressively. !18802
+- Fix Runner contacted at tooltip cache. !18810
+- Added support for LFS Download in the importing process. !18871
+- Fix issue board bug with long strings in titles. !18924
+- Does not log failed sign-in attempts when the database is in read-only mode. !18957
+- Fixes 500 error on /estimate BIG_VALUE. !18964 (Jacopo Beschi @jacopo-beschi)
+- Forbid to patch traces for finished jobs. !18969
+- Do not allow to trigger manual actions that were skipped. !18985
+- Renamed 'Overview' to 'Project' in collapsed contextual navigation at a project level. !18996 (Constance Okoghenun)
+- Fixed bug where generated api urls didn't add the base url if set. !19003
+- Fixed badge api endpoint route when relative url is set. !19004
+- Fixes: Runners search input placeholder is cut off. !19015 (Jacopo Beschi @jacopo-beschi)
+- Exclude CI_PIPELINE_ID from variables supported in dynamic environment name. !19032
+- Updates updated_at on label changes. !19065 (Jacopo Beschi @jacopo-beschi)
+- Disallow updating job status if the job is not running. !19101
+- Fix FreeBSD can not upload artifacts due to wrong tmp path. !19148
+- Check for nil AutoDevOps when saving project CI/CD settings. !19190
+- Missing timeout value in object storage pre-authorization. !19201
+- Use strings as properties key in kubernetes service spec. !19265 (Jasper Maes)
+- Fixed HTTP_PROXY environment not honored when reading remote traces. !19282 (NLR)
+- Updates ReactiveCaching clear_reactive_caching method to clear both data and alive caching. !19311
+- Fixes the styling on the modal headers. !19312 (samdbeckham)
+- Fixes a spelling error on the new label page. !19316 (samdbeckham)
+- Rails5 fix arel from. !19340 (Jasper Maes)
+- Support rails5 in postgres indexes function and fix some migrations. !19400 (Jasper Maes)
+- Fix repository archive generation when hashed storage is enabled. !19441
+- Rails 5 fix unknown keywords: changes, key_id, project, gl_repository, action, secret_token, protocol. !19466 (Jasper Maes)
+- Rails 5 fix glob spec. !19469 (Jasper Maes)
+- Showing project import_status in a humanized form no longer gives an error. !19470
+- Make avatars/icons hidden on mobile. !19585 (Takuya Noguchi)
+- Fix active tab highlight when creating new merge request. !19781 (Jan Beckmann)
+- Fixes Web IDE button on merge requests when GitLab is installed with relative URL.
+- Unverified hover state color changed to black.
+- Fix &nbsp; after sign-in with Google button.
+- Don't trim incoming emails that create new issues. (Cameron Crockett)
+- Wrapping problem on the issues page has been fixed.
+- Fix resolvable check if note's commit could not be found.
+- Fix filename matching when processing file or blob search results.
+- Allow maintainers to retry pipelines on forked projects (if allowed in merge request).
+- Fix deletion of Object Store uploads.
+- Fix overflowing Failed Jobs table in sm viewports on IE11.
+- Adjust insufficient diff hunks being persisted on NoteDiffFile.
+- Render calendar feed inline when accessed from GitLab.
+- Line height fixed. (Murat Dogan)
+- Use upload ID for creating lease key for file uploaders.
+- Use Github repo visibility during import while respecting restricted visibility levels.
+- Adjust permitted params filtering on merge scheduling.
+- Fix unscrollable Markdown preview of WebIDE on Firefox.
+- Enforce UTF-8 encoding on user input in LogrageWithTimestamp formatter and filter out file content from logs.
+- Fix project destruction failing due to idle in transaction timeouts.
+- Add a unique and not null constraint on the project_features.project_id column.
+- Expire Wiki content cache after importing a repository.
+- Fix admin counters not working when PostgreSQL has secondaries.
+- Fix backup creation and restore for specific Rake tasks.
+- Fix cross-origin errors when attempting to download JavaScript attachments.
+- Fix api_json.log not always reporting the right HTTP status code.
+- Fix attr_encryption key settings.
+- Remove gray button styles.
+- Fix print styles for markdown pages.
+
+### Deprecated (4 changes)
+
+- Deprecate Gemnasium project service. !18954
+- Rephrasing Merge Request's 'allow edits from maintainer' functionality. !19061
+- Rename issue scope created-by-me to created_by_me, and assigned-to-me to assigned_to_me. !44799
+- Migrate any remaining jobs from deprecated `object_storage_upload` queue.
+
+### Changed (42 changes, 11 of them are from the community)
+
+- Add support for smarter system notes. !17164
+- Automatically accepts project/group invite by email after user signup. !17634 (Jacopo Beschi @jacopo-beschi)
+- Dynamically fetch GCP cluster creation parameters. !17806
+- Label list page redesign. !18466
+- Move discussion actions to the right for small viewports. !18476 (George Tsiolis)
+- Add 2FA filter to the group members page. !18483
+- made listing and showing public issue apis available without authentication. !18638 (haseebeqx)
+- Refactoring UrlValidators to include url blocking. !18686
+- Removed "(Beta)" from "Auto DevOps" messages. !18759
+- Expose runner ip address to runners API. !18799 (Lars Greiss)
+- Moves MR widget external link icon to the right. !18828 (Jacopo Beschi @jacopo-beschi)
+- Add support for 'active' setting on Runner Registration API endpoint. !18848
+- Add dot to separate system notes content. !18864
+- Remove modalbox confirmation when retrying a pipeline. !18879
+- Remove docker pull prefix from registry clipboard feature. !18933 (Lars Greiss)
+- Move project sidebar sub-entries 'Environments' and 'Kubernetes' from 'CI/CD' to a new entry 'Operations'. !18941
+- Updated icons for branch and tag names in commit details. !18953 (Constance Okoghenun)
+- Expose readme url in Project API. !18960 (Imre Farkas)
+- Changes keyboard shortcut of Activity feed to `g v`. !19002
+- Updated Mattermost integration to use API v4 and only allow creation of Mattermost slash commands in the current user's teams. !19043 (Harrison Healey)
+- Add shortcuts to Web IDE docs and modal. !19044
+- Rename merge request widget author component. !19079 (George Tsiolis)
+- Rename the Master role to Maintainer. !19080
+- Use "right now" for short time periods. !19095
+- Update 404 and 403 pages with helpful actions. !19096
+- Add username to terms message in git and API calls. !19126
+- Change the IDE file buttons for an "Open in file view" button. !19129 (Sam Beckham)
+- Removes redundant script failure message from Job page. !19138
+- Add flash notice if user has already accepted terms and allow users to continue to root path. !19156
+- Redesign group settings page into expandable sections. !19184
+- Hashed Storage: migration rake task now can be executed to specific project. !19268
+- Make CI job update entrypoint to work as keep-alive endpoint. !19543
+- Avoid checking the user format in every url validation. !19575
+- Apply notification settings level of groups to all child objects.
+- Support restoring repositories into gitaly.
+- Bump omniauth-gitlab to 1.0.3.
+- Move API group deletion to Sidekiq.
+- Improve Failed Jobs tab in the Pipeline detail page.
+- Add additional theme color options.
+- Include milestones from parent groups when assigning a milestone to an issue or merge request.
+- Restore API v3 user endpoint.
+- Hide merge request option in IDE when disabled.
+
+### Performance (28 changes, 1 of them is from the community)
+
+- Add backgound migration for filling nullfied file_store columns. !18557
+- Add a cronworker to rescue stale live traces. !18680
+- Move SquashBeforeMerge vue component. !18813 (George Tsiolis)
+- Add index on runner_type for ci_runners. !18897
+- Fix CarrierWave reads local files into memoery when migrates to ObjectStorage. !19102
+- Remove double-checked internal id generation. !19181
+- Throttle updates to Project#last_repository_updated_at. !19183
+- Add background migrations for archiving legacy job traces. !19194
+- Use NPM provided version of SortableJS. !19274
+- Improve performance of group issues filtering on GitLab.com. !19429
+- Improve performance of LFS integrity check. !19494
+- Fix an N+1 when loading user avatars.
+- Only preload member records for the relevant projects/groups/user in projects API.
+- Fix some sources of excessive query counts when calculating notification recipients.
+- Optimise PagesWorker usage.
+- Optimise paused runners to reduce amount of used requests.
+- Update runner cached informations without performing validations.
+- Improve performance of project pipelines pages.
+- Persist truncated note diffs on a new table.
+- Remove unused running_or_pending_build_count.
+- Remove N+1 query for author in issues API.
+- Eliminate N+1 queries with authors and push_data_payload in Events API.
+- Eliminate cached N+1 queries for projects in Issue API.
+- Eliminate N+1 queries for CI job artifacts in /api/prjoects/:id/pipelines/:pipeline_id/jobs.
+- Fix N+1 with source_projects in merge requests API.
+- Replace grape-route-helpers with our own grape-path-helpers.
+- Move PR IO operations out of a transaction.
+- Improve performance of GroupsController#show.
+
+### Added (25 changes, 10 of them are from the community)
+
+- Closes MR check out branch modal with escape. (19050)
+- Allow changing the default favicon to a custom icon. !14497 (Alexis Reigel)
+- Export assigned issues in iCalendar feed. !17783 (Imre Farkas)
+- When MR becomes unmergeable, notify and create todo for author and merge user. !18042
+- Display help text below auto devops domain with nip.io domain name (#45561). !18496
+- Add per-project pipeline id. !18558
+- New design for wiki page deletion confirmation. !18712 (Constance Okoghenun)
+- Updates updated_at on issuable when setting time spent. !18757 (Jacopo Beschi @jacopo-beschi)
+- Expose artifacts_expire_at field for job entity in api. !18872 (Semyon Pupkov)
+- Add support for variables expression pattern matching syntax. !18902
+- Add API endpoint to render markdown text. !18926 (@blackst0ne)
+- Add `Squash and merge` to GitLab Core (CE). !18956 (@blackst0ne)
+- Adds keyboard shortcut `g k` for Kubernetes on Project pages. !19002
+- Adds keyboard shortcut `g e` for Environments on Project pages. !19002
+- Setup graphql with initial project & merge request query. !19008
+- Adds JupyterHub to cluster applications. !19019
+- Added ability to search by wiki titles. !19112
+- Add Avatar API. !19121 (Imre Farkas)
+- Add variables to POST api/v4/projects/:id/pipeline. !19124 (Jacopo Beschi @jacopo-beschi)
+- Add deploy strategies to the Auto DevOps settings. !19172
+- Automatize Deploy Token creation for Auto Devops. !19507
+- Add anchor for incoming email regex.
+- Support direct_upload with S3 Multipart uploads.
+- Add Open in Xcode link for xcode repositories.
+- Add pipeline status to the status bar of the Web IDE.
+
+### Other (40 changes, 17 of them are from the community)
+
+- Expand documentation for Runners API. !16484
+- Order UsersController#projects.json by updated_at. !18227 (Takuya Noguchi)
+- Replace the `project/issues/references.feature` spinach test with an rspec analog. !18769 (@blackst0ne)
+- Replace the `project/merge_requests/references.feature` spinach test with an rspec analog. !18794 (@blackst0ne)
+- Replace the `project/deploy_keys.feature` spinach test with an rspec analog. !18796 (@blackst0ne)
+- Replace the `project/ff_merge_requests.feature` spinach test with an rspec analog. !18800 (@blackst0ne)
+- Apply NestingDepth (level 5) (pages/pipelines.scss). !18830 (Takuya Noguchi)
+- Replace the `project/forked_merge_requests.feature` spinach test with an rspec analog. !18867 (@blackst0ne)
+- Remove Spinach. !18869 (@blackst0ne)
+- Add NOT NULL constraints to project_authorizations. !18980
+- Add helpful messages to empty wiki view. !19007
+- Increase text limit for GPG keys (mysql only). !19069
+- Take two for MR metrics population background migration. !19097
+- Remove Gemnasium badge from project README.md. !19136 (Takuya Noguchi)
+- Update awesome_print to 1.8.0. !19163 (Takuya Noguchi)
+- Update email_spec to 2.2.0. !19164 (Takuya Noguchi)
+- Update redis-namespace to 1.6.0. !19166 (Takuya Noguchi)
+- Update rdoc to 6.0.4. !19167 (Takuya Noguchi)
+- Updates the version of kubeclient from 3.0 to 3.1.0. !19199
+- Fix UI broken in line profiling modal due to Bootstrap 4. !19253 (Takuya Noguchi)
+- Add migration to disable the usage of DSA keys. !19299
+- Use the default strings of timeago.js for timeago. !19350 (Takuya Noguchi)
+- Update selenium-webdriver to 3.12.0. !19351 (Takuya Noguchi)
+- Include username in output when testing SSH to GitLab. !19358
+- Update screenshot in Gitlab.com integration documentation. !19433 (Tuğçe Nur Taş)
+- Users can accept terms during registration. !19583
+- Fix issue count on sidebar.
+- Add merge requests list endpoint for groups.
+- Upgrade GitLab from Bootstrap 3 to 4.
+- Make ActiveRecordSubscriber rails 5 compatible.
+- Show a more helpful error for import status.
+- Log response body to production_json.log when a controller responds with a 422 status.
+- Log Workhorse queue duration for Grape API calls.
+- Adjust SQL and transaction Prometheus buckets.
+- Adding branches through the WebUI is handled by Gitaly.
+- Remove shellout implementation for Repository checksums.
+- Refs containting sha checks are done by Gitaly.
+- Finding a wiki page is done by Gitaly by default.
+- Workhorse will use Gitaly to create archives.
+- Workhorse to send raw diff and patch for commits.
+
+
+## 10.8.5 (2018-06-21)
+
+### Security (5 changes)
+
+- Fix XSS vulnerability for table of content generation.
+- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
+- HTML escape branch name in project graphs page.
+- HTML escape the name of the user in ProjectsHelper#link_to_member.
+- Don't show events from internal projects for anonymous users in public feed.
+
+
## 10.8.4 (2018-06-06)
- No changes.
@@ -220,6 +504,22 @@ entry.
- Gitaly handles repository forks by default.
+## 10.7.6 (2018-06-21)
+
+### Security (6 changes)
+
+- Fix XSS vulnerability for table of content generation.
+- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
+- HTML escape branch name in project graphs page.
+- HTML escape the name of the user in ProjectsHelper#link_to_member.
+- Don't show events from internal projects for anonymous users in public feed.
+- XSS fix to use safe_params instead of params in url_for helpers.
+
+### Other (1 change)
+
+- Replacing gollum libraries for gitlab custom libs. !18343
+
+
## 10.7.5 (2018-05-28)
### Security (3 changes)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f7b12e17c70..fd4e769ecee 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -650,7 +650,7 @@ the feature you contribute through all of these steps.
1. Working and clean code that is commented where needed
1. [Unit, integration, and system tests][testing] that pass on the CI server
1. Performance/scalability implications have been considered, addressed, and tested
-1. [Documented][doc-styleguide] in the `/doc` directory
+1. [Documented][doc-guidelines] in the `/doc` directory
1. [Changelog entry added][changelog], if necessary
1. Reviewed and any concerns are addressed
1. Merged by a project maintainer
@@ -687,7 +687,7 @@ merge request:
contributors to enhance security
1. [Database Migrations](doc/development/migration_style_guide.md)
1. [Markdown](http://www.cirosantilli.com/markdown-styleguide)
-1. [Documentation styleguide][doc-styleguide]
+1. [Documentation styleguide](https://docs.gitlab.com/ee/development/documentation/styleguide.html)
1. Interface text should be written subjectively instead of objectively. It
should be the GitLab core team addressing a person. It should be written in
present time and never use past tense (has been/was). For example instead
diff --git a/Gemfile b/Gemfile
index 945b5486437..93c6115eeec 100644
--- a/Gemfile
+++ b/Gemfile
@@ -230,7 +230,7 @@ gem 'ruby-fogbugz', '~> 0.2.1'
gem 'kubeclient', '~> 3.1.0'
# Sanitize user input
-gem 'sanitize', '~> 2.0'
+gem 'sanitize', '~> 4.6.5'
gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input
diff --git a/Gemfile.lock b/Gemfile.lock
index 3c619b2e534..8281c1eff9a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -295,13 +295,13 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-gollum-lib (4.2.7.4)
+ gitlab-gollum-lib (4.2.7.5)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1)
- sanitize (~> 2.1)
+ sanitize (~> 4.6.4)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15)
@@ -514,6 +514,8 @@ GEM
netrc (0.11.0)
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
+ nokogumbo (1.5.0)
+ nokogiri
numerizer (0.1.1)
oauth (0.5.4)
oauth2 (1.4.0)
@@ -804,8 +806,10 @@ GEM
et-orbi (~> 1.0)
rugged (0.27.2)
safe_yaml (1.0.4)
- sanitize (2.1.0)
+ sanitize (4.6.5)
+ crass (~> 1.0.2)
nokogiri (>= 1.4.4)
+ nokogumbo (~> 1.4)
sass (3.5.5)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
@@ -867,7 +871,7 @@ GEM
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
- sprockets (3.7.1)
+ sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
@@ -1151,7 +1155,7 @@ DEPENDENCIES
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
- sanitize (~> 2.0)
+ sanitize (~> 4.6.5)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 679318b9be5..52388f17c7c 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -298,13 +298,13 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-gollum-lib (4.2.7.4)
+ gitlab-gollum-lib (4.2.7.5)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1)
- sanitize (~> 2.1)
+ sanitize (~> 4.6.4)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15)
@@ -518,6 +518,8 @@ GEM
nio4r (2.3.1)
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
+ nokogumbo (1.5.0)
+ nokogiri
numerizer (0.1.1)
oauth (0.5.4)
oauth2 (1.4.0)
@@ -813,8 +815,10 @@ GEM
et-orbi (~> 1.0)
rugged (0.27.1)
safe_yaml (1.0.4)
- sanitize (2.1.0)
+ sanitize (4.6.5)
+ crass (~> 1.0.2)
nokogiri (>= 1.4.4)
+ nokogumbo (~> 1.4)
sass (3.5.5)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
@@ -1162,7 +1166,7 @@ DEPENDENCIES
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
- sanitize (~> 2.0)
+ sanitize (~> 4.6.5)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
diff --git a/VERSION b/VERSION
index 8ca9077d87b..0116f5d2c81 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-11.0.0-pre
+11.1.0-pre
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js
index bd08308904c..54e86f329e4 100644
--- a/app/assets/javascripts/ajax_loading_spinner.js
+++ b/app/assets/javascripts/ajax_loading_spinner.js
@@ -26,7 +26,7 @@ export default class AjaxLoadingSpinner {
}
static toggleLoadingIcon(iconElement) {
- const classList = iconElement.classList;
+ const { classList } = iconElement;
classList.toggle(iconElement.dataset.icon);
classList.toggle('fa-spinner');
classList.toggle('fa-spin');
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 959e6289502..fa00a3cf386 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -31,7 +31,9 @@ export default class Autosave {
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
const event = new Event('change', { bubbles: true, cancelable: false });
const field = this.field.get(0);
- field.dispatchEvent(event);
+ if (field) {
+ field.dispatchEvent(event);
+ }
}
save() {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index eb0f06efab4..70f20c5c7cf 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -11,7 +11,8 @@ import axios from './lib/utils/axios_utils';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
-const requestAnimationFrame = window.requestAnimationFrame ||
+const requestAnimationFrame =
+ window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.setTimeout;
@@ -37,21 +38,28 @@ class AwardsHandler {
this.emoji = emoji;
this.eventListeners = [];
// If the user shows intent let's pre-build the menu
- this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
- const $menu = $('.emoji-menu');
- if ($menu.length === 0) {
- requestAnimationFrame(() => {
- this.createEmojiMenu();
- });
- }
- });
- this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
+ this.registerEventListener(
+ 'one',
+ $(document),
+ 'mouseenter focus',
+ '.js-add-award',
+ 'mouseenter focus',
+ () => {
+ const $menu = $('.emoji-menu');
+ if ($menu.length === 0) {
+ requestAnimationFrame(() => {
+ this.createEmojiMenu();
+ });
+ }
+ },
+ );
+ this.registerEventListener('on', $(document), 'click', '.js-add-award', e => {
e.stopPropagation();
e.preventDefault();
this.showEmojiMenu($(e.currentTarget));
});
- this.registerEventListener('on', $('html'), 'click', (e) => {
+ this.registerEventListener('on', $('html'), 'click', e => {
const $target = $(e.target);
if (!$target.closest('.emoji-menu').length) {
$('.js-awards-block.current').removeClass('current');
@@ -61,12 +69,14 @@ class AwardsHandler {
}
}
});
- this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => {
+ this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', e => {
e.preventDefault();
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon');
- const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
+ const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data(
+ 'name',
+ );
$target.closest('.js-awards-block').addClass('current');
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName);
@@ -83,7 +93,10 @@ class AwardsHandler {
showEmojiMenu($addBtn) {
if ($addBtn.hasClass('js-note-emoji')) {
- $addBtn.closest('.note').find('.js-awards-block').addClass('current');
+ $addBtn
+ .closest('.note')
+ .find('.js-awards-block')
+ .addClass('current');
} else {
$addBtn.closest('.js-awards-block').addClass('current');
}
@@ -177,32 +190,38 @@ class AwardsHandler {
const remainingCategories = Object.keys(categoryMap).slice(1);
const allCategoriesAddedPromise = remainingCategories.reduce(
(promiseChain, categoryNameKey) =>
- promiseChain.then(() =>
- new Promise((resolve) => {
- const emojisInCategory = categoryMap[categoryNameKey];
- const categoryMarkup = this.renderCategory(
- categoryLabelMap[categoryNameKey],
- emojisInCategory,
- );
- requestAnimationFrame(() => {
- emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
- resolve();
- });
- }),
- ),
+ promiseChain.then(
+ () =>
+ new Promise(resolve => {
+ const emojisInCategory = categoryMap[categoryNameKey];
+ const categoryMarkup = this.renderCategory(
+ categoryLabelMap[categoryNameKey],
+ emojisInCategory,
+ );
+ requestAnimationFrame(() => {
+ emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
+ resolve();
+ });
+ }),
+ ),
Promise.resolve(),
);
- allCategoriesAddedPromise.then(() => {
- // Used for tests
- // We check for the menu in case it was destroyed in the meantime
- if (menu) {
- menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
- }
- }).catch((err) => {
- emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>');
- throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
- });
+ allCategoriesAddedPromise
+ .then(() => {
+ // Used for tests
+ // We check for the menu in case it was destroyed in the meantime
+ if (menu) {
+ menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
+ }
+ })
+ .catch(err => {
+ emojiContentElement.insertAdjacentHTML(
+ 'beforeend',
+ '<p>We encountered an error while adding the remaining categories</p>',
+ );
+ throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
+ });
}
renderCategory(name, emojiList, opts = {}) {
@@ -211,7 +230,9 @@ class AwardsHandler {
${name}
</h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
- ${emojiList.map(emojiName => `
+ ${emojiList
+ .map(
+ emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${this.emoji.glEmojiTag(emojiName, {
@@ -219,7 +240,9 @@ class AwardsHandler {
})}
</button>
</li>
- `).join('\n')}
+ `,
+ )
+ .join('\n')}
</ul>
`;
}
@@ -232,7 +255,7 @@ class AwardsHandler {
top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`,
};
if (position === 'right') {
- css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`;
+ css.left = `${$addBtn.offset().left - $menu.outerWidth() + 20}px`;
$menu.addClass('is-aligned-right');
} else {
css.left = `${$addBtn.offset().left}px`;
@@ -416,7 +439,10 @@ class AwardsHandler {
</button>
`;
const $emojiButton = $(buttonHtml);
- $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('name', emojiName);
+ $emojiButton
+ .insertBefore(votesBlock.find('.js-award-holder'))
+ .find('.emoji-icon')
+ .data('name', emojiName);
this.animateEmoji($emojiButton);
$('.award-control').tooltip();
votesBlock.removeClass('current');
@@ -426,7 +452,7 @@ class AwardsHandler {
const className = 'pulse animated once short';
$emoji.addClass(className);
- this.registerEventListener('on', $emoji, animationEndEventString, (e) => {
+ this.registerEventListener('on', $emoji, animationEndEventString, e => {
$(e.currentTarget).removeClass(className);
});
}
@@ -444,15 +470,16 @@ class AwardsHandler {
if (this.isUserAuthored($emojiButton)) {
this.userAuthored($emojiButton);
} else {
- axios.post(awardUrl, {
- name: emoji,
- })
- .then(({ data }) => {
- if (data.ok) {
- callback();
- }
- })
- .catch(() => flash(__('Something went wrong on our end.')));
+ axios
+ .post(awardUrl, {
+ name: emoji,
+ })
+ .then(({ data }) => {
+ if (data.ok) {
+ callback();
+ }
+ })
+ .catch(() => flash(__('Something went wrong on our end.')));
}
}
@@ -486,26 +513,33 @@ class AwardsHandler {
}
getFrequentlyUsedEmojis() {
- return this.frequentlyUsedEmojis || (() => {
- const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
- this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
- inputName => this.emoji.isEmojiNameValid(inputName),
- );
-
- return this.frequentlyUsedEmojis;
- })();
+ return (
+ this.frequentlyUsedEmojis ||
+ (() => {
+ const frequentlyUsedEmojis = _.uniq(
+ (Cookies.get('frequently_used_emojis') || '').split(','),
+ );
+ this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(inputName =>
+ this.emoji.isEmojiNameValid(inputName),
+ );
+
+ return this.frequentlyUsedEmojis;
+ })()
+ );
}
setupSearch() {
const $search = $('.js-emoji-menu-search');
- this.registerEventListener('on', $search, 'input', (e) => {
- const term = $(e.target).val().trim();
+ this.registerEventListener('on', $search, 'input', e => {
+ const term = $(e.target)
+ .val()
+ .trim();
this.searchEmojis(term);
});
const $menu = $('.emoji-menu');
- this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
+ this.registerEventListener('on', $menu, transitionEndEventString, e => {
if (e.target === e.currentTarget) {
// Clear the search
this.searchEmojis('');
@@ -523,19 +557,26 @@ class AwardsHandler {
// Generate a search result block
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
const foundEmojis = this.findMatchingEmojiElements(term).show();
- const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
+ const ul = $('<ul>')
+ .addClass('emoji-menu-list emoji-menu-search')
+ .append(foundEmojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
- $('.emoji-menu-content').append(h5).append(ul);
+ $('.emoji-menu-content')
+ .append(h5)
+ .append(ul);
} else {
- $('.emoji-menu-content').children().show();
+ $('.emoji-menu-content')
+ .children()
+ .show();
}
}
findMatchingEmojiElements(query) {
const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
- const $matchingElements = $emojiElements
- .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
+ const $matchingElements = $emojiElements.filter(
+ (i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
+ );
return $matchingElements.closest('li').clone();
}
@@ -550,16 +591,13 @@ class AwardsHandler {
$emojiMenu.addClass(IS_RENDERED);
// enqueues animation as a microtask, so it begins ASAP once IS_RENDERED added
- return Promise.resolve()
- .then(() => $emojiMenu.addClass(IS_VISIBLE));
+ return Promise.resolve().then(() => $emojiMenu.addClass(IS_VISIBLE));
}
hideMenuElement($emojiMenu) {
- $emojiMenu.on(transitionEndEventString, (e) => {
+ $emojiMenu.on(transitionEndEventString, e => {
if (e.currentTarget === e.target) {
- $emojiMenu
- .removeClass(IS_RENDERED)
- .off(transitionEndEventString);
+ $emojiMenu.removeClass(IS_RENDERED).off(transitionEndEventString);
}
});
@@ -567,7 +605,7 @@ class AwardsHandler {
}
destroy() {
- this.eventListeners.forEach((entry) => {
+ this.eventListeners.forEach(entry => {
entry.element.off.call(entry.element, ...entry.args);
});
$('.emoji-menu').remove();
@@ -577,8 +615,9 @@ class AwardsHandler {
let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
- awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji')
- .then(Emoji => new AwardsHandler(Emoji));
+ awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
+ Emoji => new AwardsHandler(Emoji),
+ );
}
return awardsHandlerPromise;
}
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index 75834ba351d..00419e80cbb 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -52,7 +52,7 @@ export default function initCopyToClipboard() {
* data types to the intended values.
*/
$(document).on('copy', 'body > textarea[readonly]', (e) => {
- const clipboardData = e.originalEvent.clipboardData;
+ const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const text = e.target.value;
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index 9745e37acce..5d7a3bed301 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -321,7 +321,7 @@ export class CopyAsGFM {
}
static copyAsGFM(e, transformer) {
- const clipboardData = e.originalEvent.clipboardData;
+ const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const documentFragment = getSelectedFragment();
@@ -338,7 +338,7 @@ export class CopyAsGFM {
}
static pasteGFM(e) {
- const clipboardData = e.originalEvent.clipboardData;
+ const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const text = clipboardData.getData('text/plain');
diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
index 766039404ce..7986287f7e7 100644
--- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
@@ -84,7 +84,7 @@ class BalsamiqViewer {
renderTemplate(preview) {
const resource = this.getResource(preview.resourceID);
const name = BalsamiqViewer.parseTitle(resource);
- const image = preview.image;
+ const { image } = preview;
const template = PREVIEW_TEMPLATE({
name,
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index 06ef86ecb77..b88e69a07bf 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -12,7 +12,7 @@ export default function loadBalsamiqFile() {
if (!(viewer instanceof Element)) return;
- const endpoint = viewer.dataset.endpoint;
+ const { endpoint } = viewer.dataset;
const balsamiqViewer = new BalsamiqViewer(viewer);
balsamiqViewer.loadFile(endpoint).catch(onError);
diff --git a/app/assets/javascripts/blob/stl_viewer.js b/app/assets/javascripts/blob/stl_viewer.js
index 63236b6477f..339906adc34 100644
--- a/app/assets/javascripts/blob/stl_viewer.js
+++ b/app/assets/javascripts/blob/stl_viewer.js
@@ -5,7 +5,7 @@ export default () => {
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
el.addEventListener('click', (e) => {
- const target = e.target;
+ const { target } = e;
e.preventDefault();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index b717c4b0fd4..371be109229 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -6,13 +6,13 @@ import Flash from '../../flash';
import { __ } from '../../locale';
import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub';
-import assigneeTitle from '../../sidebar/components/assignees/assignee_title.vue';
-import assignees from '../../sidebar/components/assignees/assignees.vue';
+import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue';
+import Assignees from '../../sidebar/components/assignees/assignees.vue';
import DueDateSelectors from '../../due_date_select';
-import './sidebar/remove_issue';
+import RemoveBtn from './sidebar/remove_issue.vue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
-import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
+import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import MilestoneSelect from '../../milestone_select';
const Store = gl.issueBoards.BoardsStore;
@@ -22,10 +22,10 @@ window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSidebar = Vue.extend({
components: {
- assigneeTitle,
- assignees,
- removeBtn: gl.issueBoards.RemoveIssueBtn,
- subscriptions,
+ AssigneeTitle,
+ Assignees,
+ RemoveBtn,
+ Subscriptions,
},
props: {
currentUser: {
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.vue
index 888bc9d7ef2..dbd69f84526 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -1,8 +1,8 @@
-import Vue from 'vue';
+<script>
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
-gl.issueBoards.ModalEmptyState = Vue.extend({
+export default {
mixins: [modalMixin],
props: {
newIssuePath: {
@@ -38,32 +38,36 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
return obj;
},
},
- template: `
- <section class="empty-state">
- <div class="row">
- <div class="col-12 col-md-6 order-md-last">
- <aside class="svg-content"><img :src="emptyStateSvg"/></aside>
- </div>
- <div class="col-12 col-md-6 order-md-first">
- <div class="text-content">
- <h4>{{ contents.title }}</h4>
- <p v-html="contents.content"></p>
- <a
- :href="newIssuePath"
- class="btn btn-success btn-inverted"
- v-if="activeTab === 'all'">
- New issue
- </a>
- <button
- type="button"
- class="btn btn-default"
- @click="changeTab('all')"
- v-if="activeTab === 'selected'">
- Open issues
- </button>
- </div>
+};
+</script>
+
+<template>
+ <section class="empty-state">
+ <div class="row">
+ <div class="col-12 col-md-6 order-md-last">
+ <aside class="svg-content"><img :src="emptyStateSvg"/></aside>
+ </div>
+ <div class="col-12 col-md-6 order-md-first">
+ <div class="text-content">
+ <h4>{{ contents.title }}</h4>
+ <p v-html="contents.content"></p>
+ <a
+ v-if="activeTab === 'all'"
+ :href="newIssuePath"
+ class="btn btn-success btn-inverted"
+ >
+ New issue
+ </a>
+ <button
+ v-if="activeTab === 'selected'"
+ class="btn btn-default"
+ type="button"
+ @click="changeTab('all')"
+ >
+ Open issues
+ </button>
</div>
</div>
- </section>
- `,
-});
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.vue
index 2745ca219ad..e0dac6003f1 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -1,14 +1,14 @@
-import Vue from 'vue';
+<script>
import Flash from '../../../flash';
import { __ } from '../../../locale';
-import './lists_dropdown';
+import ListsDropdown from './lists_dropdown.vue';
import { pluralize } from '../../../lib/utils/text_utility';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
-gl.issueBoards.ModalFooter = Vue.extend({
+export default {
components: {
- 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
+ ListsDropdown,
},
mixins: [modalMixin],
data() {
@@ -55,28 +55,32 @@ gl.issueBoards.ModalFooter = Vue.extend({
this.toggleModal(false);
},
},
- template: `
- <footer
- class="form-actions add-issues-footer">
- <div class="float-left">
- <button
- class="btn btn-success"
- type="button"
- :disabled="submitDisabled"
- @click="addIssues">
- {{ submitText }}
- </button>
- <span class="inline add-issues-footer-to-list">
- to list
- </span>
- <lists-dropdown></lists-dropdown>
- </div>
+};
+</script>
+<template>
+ <footer
+ class="form-actions add-issues-footer"
+ >
+ <div class="float-left">
<button
- class="btn btn-default float-right"
+ :disabled="submitDisabled"
+ class="btn btn-success"
type="button"
- @click="toggleModal(false)">
- Cancel
+ @click="addIssues"
+ >
+ {{ submitText }}
</button>
- </footer>
- `,
-});
+ <span class="inline add-issues-footer-to-list">
+ to list
+ </span>
+ <lists-dropdown/>
+ </div>
+ <button
+ class="btn btn-default float-right"
+ type="button"
+ @click="toggleModal(false)"
+ >
+ Cancel
+ </button>
+ </footer>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js
index 5e511bb8935..cc9848058ca 100644
--- a/app/assets/javascripts/boards/components/modal/header.js
+++ b/app/assets/javascripts/boards/components/modal/header.js
@@ -1,12 +1,12 @@
import Vue from 'vue';
import modalFilters from './filters';
-import './tabs';
+import modalTabs from './tabs.vue';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalHeader = Vue.extend({
components: {
- 'modal-tabs': gl.issueBoards.ModalTabs,
+ modalTabs,
modalFilters,
},
mixins: [modalMixin],
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index c8b2f45f177..983061f52ae 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -5,16 +5,16 @@ import queryData from '~/boards/utils/query_data';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import './header';
import './list';
-import './footer';
-import './empty_state';
+import ModalFooter from './footer.vue';
+import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store';
gl.issueBoards.IssuesModal = Vue.extend({
components: {
+ EmptyState,
'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList,
- 'modal-footer': gl.issueBoards.ModalFooter,
- 'empty-state': gl.issueBoards.ModalEmptyState,
+ ModalFooter,
loadingIcon,
},
props: {
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
deleted file mode 100644
index e644de2d4fc..00000000000
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import Vue from 'vue';
-import ModalStore from '../../stores/modal_store';
-
-gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
- data() {
- return {
- modal: ModalStore.store,
- state: gl.issueBoards.BoardsStore.state,
- };
- },
- computed: {
- selected() {
- return this.modal.selectedList || this.state.lists[1];
- },
- },
- destroyed() {
- this.modal.selectedList = null;
- },
- template: `
- <div class="dropdown inline">
- <button
- class="dropdown-menu-toggle"
- type="button"
- data-toggle="dropdown"
- aria-expanded="false">
- <span
- class="dropdown-label-box"
- :style="{ backgroundColor: selected.label.color }">
- </span>
- {{ selected.title }}
- <i class="fa fa-chevron-down"></i>
- </button>
- <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
- <ul>
- <li
- v-for="list in state.lists"
- v-if="list.type == 'label'">
- <a
- href="#"
- role="button"
- :class="{ 'is-active': list.id == selected.id }"
- @click.prevent="modal.selectedList = list">
- <span
- class="dropdown-label-box"
- :style="{ backgroundColor: list.label.color }">
- </span>
- {{ list.title }}
- </a>
- </li>
- </ul>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
new file mode 100644
index 00000000000..6a5a39099bd
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
@@ -0,0 +1,56 @@
+<script>
+import ModalStore from '../../stores/modal_store';
+
+export default {
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ selected() {
+ return this.modal.selectedList || this.state.lists[1];
+ },
+ },
+ destroyed() {
+ this.modal.selectedList = null;
+ },
+};
+</script>
+<template>
+ <div class="dropdown inline">
+ <button
+ class="dropdown-menu-toggle"
+ type="button"
+ data-toggle="dropdown"
+ aria-expanded="false">
+ <span
+ :style="{ backgroundColor: selected.label.color }"
+ class="dropdown-label-box">
+ </span>
+ {{ selected.title }}
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+ <ul>
+ <li
+ v-for="(list, i) in state.lists"
+ v-if="list.type == 'label'"
+ :key="i">
+ <a
+ :class="{ 'is-active': list.id == selected.id }"
+ href="#"
+ role="button"
+ @click.prevent="modal.selectedList = list">
+ <span
+ :style="{ backgroundColor: list.label.color }"
+ class="dropdown-label-box">
+ </span>
+ {{ list.title }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
deleted file mode 100644
index 9d331de8e22..00000000000
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import Vue from 'vue';
-import ModalStore from '../../stores/modal_store';
-import modalMixin from '../../mixins/modal_mixins';
-
-gl.issueBoards.ModalTabs = Vue.extend({
- mixins: [modalMixin],
- data() {
- return ModalStore.store;
- },
- computed: {
- selectedCount() {
- return ModalStore.selectedCount();
- },
- },
- destroyed() {
- this.activeTab = 'all';
- },
- template: `
- <div class="top-area prepend-top-10 append-bottom-10">
- <ul class="nav-links issues-state-filters">
- <li :class="{ 'active': activeTab == 'all' }">
- <a
- href="#"
- role="button"
- @click.prevent="changeTab('all')">
- Open issues
- <span class="badge badge-pill">
- {{ issuesCount }}
- </span>
- </a>
- </li>
- <li :class="{ 'active': activeTab == 'selected' }">
- <a
- href="#"
- role="button"
- @click.prevent="changeTab('selected')">
- Selected issues
- <span class="badge badge-pill">
- {{ selectedCount }}
- </span>
- </a>
- </li>
- </ul>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
new file mode 100644
index 00000000000..d926b080094
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/tabs.vue
@@ -0,0 +1,49 @@
+<script>
+ import ModalStore from '../../stores/modal_store';
+ import modalMixin from '../../mixins/modal_mixins';
+
+ export default {
+ mixins: [modalMixin],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectedCount() {
+ return ModalStore.selectedCount();
+ },
+ },
+ destroyed() {
+ this.activeTab = 'all';
+ },
+ };
+</script>
+<template>
+ <div class="top-area prepend-top-10 append-bottom-10">
+ <ul class="nav-links issues-state-filters">
+ <li :class="{ 'active': activeTab == 'all' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('all')"
+ >
+ Open issues
+ <span class="badge badge-pill">
+ {{ issuesCount }}
+ </span>
+ </a>
+ </li>
+ <li :class="{ 'active': activeTab == 'selected' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('selected')"
+ >
+ Selected issues
+ <span class="badge badge-pill">
+ {{ selectedCount }}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
deleted file mode 100644
index 0a0820ec5fd..00000000000
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import Vue from 'vue';
-import Flash from '../../../flash';
-import { __ } from '../../../locale';
-
-const Store = gl.issueBoards.BoardsStore;
-
-window.gl = window.gl || {};
-window.gl.issueBoards = window.gl.issueBoards || {};
-
-gl.issueBoards.RemoveIssueBtn = Vue.extend({
- props: {
- issue: {
- type: Object,
- required: true,
- },
- list: {
- type: Object,
- required: true,
- },
- },
- computed: {
- updateUrl() {
- return this.issue.path;
- },
- },
- methods: {
- removeIssue() {
- const issue = this.issue;
- const lists = issue.getLists();
- const listLabelIds = lists.map(list => list.label.id);
-
- let labelIds = issue.labels
- .map(label => label.id)
- .filter(id => !listLabelIds.includes(id));
- if (labelIds.length === 0) {
- labelIds = [''];
- }
-
- const data = {
- issue: {
- label_ids: labelIds,
- },
- };
-
- // Post the remove data
- Vue.http.patch(this.updateUrl, data).catch(() => {
- Flash(__('Failed to remove issue from board, please try again.'));
-
- lists.forEach((list) => {
- list.addIssue(issue);
- });
- });
-
- // Remove from the frontend store
- lists.forEach((list) => {
- list.removeIssue(issue);
- });
-
- Store.detail.issue = {};
- },
- },
- template: `
- <div
- class="block list">
- <button
- class="btn btn-default btn-block"
- type="button"
- @click="removeIssue">
- Remove from board
- </button>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
new file mode 100644
index 00000000000..55278626ffc
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -0,0 +1,72 @@
+<script>
+ import Vue from 'vue';
+ import Flash from '../../../flash';
+ import { __ } from '../../../locale';
+
+ const Store = gl.issueBoards.BoardsStore;
+
+ export default {
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ updateUrl() {
+ return this.issue.path;
+ },
+ },
+ methods: {
+ removeIssue() {
+ const { issue } = this;
+ const lists = issue.getLists();
+ const listLabelIds = lists.map(list => list.label.id);
+
+ let labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
+ if (labelIds.length === 0) {
+ labelIds = [''];
+ }
+
+ const data = {
+ issue: {
+ label_ids: labelIds,
+ },
+ };
+
+ // Post the remove data
+ Vue.http.patch(this.updateUrl, data).catch(() => {
+ Flash(__('Failed to remove issue from board, please try again.'));
+
+ lists.forEach(list => {
+ list.addIssue(issue);
+ });
+ });
+
+ // Remove from the frontend store
+ lists.forEach(list => {
+ list.removeIssue(issue);
+ });
+
+ Store.detail.issue = {};
+ },
+ },
+ };
+</script>
+<template>
+ <div
+ class="block list"
+ >
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click="removeIssue"
+ >
+ Remove from board
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 2d9141bf71c..751a66f89c6 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -121,7 +121,7 @@ export default () => {
this.filterManager.updateTokens();
},
updateDetailIssue(newIssue) {
- const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
+ const { sidebarInfoEndpoint } = newIssue;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
@@ -144,7 +144,7 @@ export default () => {
Store.detail.issue = {};
},
toggleSubscription(id) {
- const issue = Store.detail.issue;
+ const { issue } = Store.detail;
if (issue.id === id && issue.toggleSubscriptionEndpoint) {
issue.setFetchingState('subscriptions', true);
BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index a4220cd840d..0d9ac367a70 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -26,7 +26,7 @@ class ModalStore {
toggleIssue(issueObj) {
const issue = issueObj;
- const selected = issue.selected;
+ const { selected } = issue;
issue.selected = !selected;
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index e42a3632e79..8139aa69fc7 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -81,7 +81,7 @@ export default class Clusters {
}
initApplications() {
- const store = this.store;
+ const { store } = this;
const el = document.querySelector('#js-cluster-applications');
this.applications = new Vue({
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 2d180e9903a..410580b4c25 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -122,7 +122,7 @@ export default class ImageFile {
return $('.swipe.view', this.file).each((function(_this) {
return function(index, view) {
var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
- ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+ ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref;
$swipeFrame = $('.swipe-frame', view);
$swipeWrap = $('.swipe-wrap', view);
$swipeBar = $('.swipe-bar', view);
@@ -159,7 +159,7 @@ export default class ImageFile {
return $('.onion-skin.view', this.file).each((function(_this) {
return function(index, view) {
var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
- ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+ ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref;
$frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view);
$track = $('.drag-track', view);
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index f77a5730b77..02aa507ba03 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -281,7 +281,7 @@ export default class CreateMergeRequestDropdown {
if (event.target === this.branchInput) {
target = 'branch';
- value = this.branchInput.value;
+ ({ value } = this.branchInput);
} else if (event.target === this.refInput) {
target = 'ref';
value =
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 40f7c2fe5f3..5528d2a542b 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -111,7 +111,7 @@ const DiffNoteAvatars = Vue.extend({
});
},
addNoCommentClass() {
- const notesCount = this.notesCount;
+ const { notesCount } = this;
$(this.$el).closest('.js-avatar-container')
.toggleClass('no-comment-btn', notesCount > 0)
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 66b20cc8739..2b893e35b6d 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -73,7 +73,7 @@ const JumpToDiscussion = Vue.extend({
}).toArray();
};
- const discussions = this.discussions;
+ const { discussions } = this;
if (activeTab === 'diffs') {
discussionsSelector = '.diffs .notes[data-discussion-id]';
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index 07f3be29090..a69b34b0db8 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -1,4 +1,3 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */
/* global CommentsStore */
/* global ResolveService */
@@ -41,54 +40,54 @@ const ResolveBtn = Vue.extend({
required: true,
},
},
- data: function () {
+ data() {
return {
discussions: CommentsStore.state,
- loading: false
+ loading: false,
};
},
computed: {
- discussion: function () {
+ discussion() {
return this.discussions[this.discussionId];
},
- note: function () {
+ note() {
return this.discussion ? this.discussion.getNote(this.noteId) : {};
},
- buttonText: function () {
+ buttonText() {
if (this.isResolved) {
return `Resolved by ${this.resolvedByName}`;
} else if (this.canResolve) {
return 'Mark as resolved';
- } else {
- return 'Unable to resolve';
}
+
+ return 'Unable to resolve';
},
- isResolved: function () {
+ isResolved() {
if (this.note) {
return this.note.resolved;
- } else {
- return false;
}
+
+ return false;
},
- resolvedByName: function () {
+ resolvedByName() {
return this.note.resolved_by;
},
},
watch: {
- 'discussions': {
+ discussions: {
handler: 'updateTooltip',
- deep: true
- }
+ deep: true,
+ },
},
- mounted: function () {
+ mounted() {
$(this.$refs.button).tooltip({
- container: 'body'
+ container: 'body',
});
},
- beforeDestroy: function () {
+ beforeDestroy() {
CommentsStore.delete(this.discussionId, this.noteId);
},
- created: function () {
+ created() {
CommentsStore.create({
discussionId: this.discussionId,
noteId: this.noteId,
@@ -101,43 +100,41 @@ const ResolveBtn = Vue.extend({
});
},
methods: {
- updateTooltip: function () {
+ updateTooltip() {
this.$nextTick(() => {
$(this.$refs.button)
.tooltip('hide')
.tooltip('_fixTitle');
});
},
- resolve: function () {
+ resolve() {
if (!this.canResolve) return;
let promise;
this.loading = true;
if (this.isResolved) {
- promise = ResolveService
- .unresolve(this.noteId);
+ promise = ResolveService.unresolve(this.noteId);
} else {
- promise = ResolveService
- .resolve(this.noteId);
+ promise = ResolveService.resolve(this.noteId);
}
promise
.then(resp => resp.json())
- .then((data) => {
+ .then(data => {
this.loading = false;
- const resolved_by = data ? data.resolved_by : null;
+ const resolvedBy = data ? data.resolved_by : null;
- CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
+ CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolvedBy);
this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus();
- document.dispatchEvent(new CustomEvent('refreshVueNotes'));
-
this.updateTooltip();
})
- .catch(() => new Flash('An error occurred when trying to resolve a comment. Please try again.'));
- }
+ .catch(
+ () => new Flash('An error occurred when trying to resolve a comment. Please try again.'),
+ );
+ },
},
});
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index 18fceac6368..7dcf3594471 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -1,5 +1,4 @@
-/* eslint-disable func-names, comma-dangle, new-cap, no-new */
-/* global ResolveCount */
+/* eslint-disable func-names, new-cap */
import $ from 'jquery';
import Vue from 'vue';
@@ -15,12 +14,13 @@ import './components/resolve_count';
import './components/resolve_discussion_btn';
import './components/diff_note_avatars';
import './components/new_issue_for_discussion';
-import { hasVueMRDiscussionsCookie } from '../lib/utils/common_utils';
export default () => {
- const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
- const projectPath = projectPathHolder.dataset.projectPath;
- const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
+ const projectPathHolder =
+ document.querySelector('.merge-request') || document.querySelector('.commit-box');
+ const { projectPath } = projectPathHolder.dataset;
+ const COMPONENT_SELECTOR =
+ 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
window.gl = window.gl || {};
window.gl.diffNoteApps = {};
@@ -28,9 +28,9 @@ export default () => {
window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
gl.diffNotesCompileComponents = () => {
- $('diff-note-avatars').each(function () {
+ $('diff-note-avatars').each(function() {
const tmp = Vue.extend({
- template: $(this).get(0).outerHTML
+ template: $(this).get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
@@ -41,12 +41,12 @@ export default () => {
});
});
- const $components = $(COMPONENT_SELECTOR).filter(function () {
+ const $components = $(COMPONENT_SELECTOR).filter(function() {
return $(this).closest('resolve-count').length !== 1;
});
if ($components) {
- $components.each(function () {
+ $components.each(function() {
const $this = $(this);
const noteId = $this.attr(':note-id');
const discussionId = $this.attr(':discussion-id');
@@ -54,7 +54,7 @@ export default () => {
if ($this.is('comment-and-resolve-btn') && !discussionId) return;
const tmp = Vue.extend({
- template: $this.get(0).outerHTML
+ template: $this.get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
@@ -69,15 +69,5 @@ export default () => {
gl.diffNotesCompileComponents();
- const resolveCountAppEl = document.querySelector('#resolve-count-app');
- if (!hasVueMRDiscussionsCookie() && resolveCountAppEl) {
- new Vue({
- el: resolveCountAppEl,
- components: {
- 'resolve-count': ResolveCount
- },
- });
- }
-
$(window).trigger('resize.nav');
};
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index d16f9297de1..0b3568e432d 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -8,8 +8,12 @@ window.gl = window.gl || {};
class ResolveServiceClass {
constructor(root) {
- this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
- this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`);
+ this.noteResource = Vue.resource(
+ `${root}/notes{/noteId}/resolve?html=true`,
+ );
+ this.discussionResource = Vue.resource(
+ `${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
+ );
}
resolve(noteId) {
@@ -33,7 +37,7 @@ class ResolveServiceClass {
promise
.then(resp => resp.json())
- .then((data) => {
+ .then(data => {
discussion.loading = false;
const resolvedBy = data ? data.resolved_by : null;
@@ -45,9 +49,13 @@ class ResolveServiceClass {
if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
- document.dispatchEvent(new CustomEvent('refreshVueNotes'));
})
- .catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
+ .catch(
+ () =>
+ new Flash(
+ 'An error occurred when trying to resolve a discussion. Please try again.',
+ ),
+ );
}
resolveAll(mergeRequestId, discussionId) {
@@ -55,10 +63,13 @@ class ResolveServiceClass {
discussion.loading = true;
- return this.discussionResource.save({
- mergeRequestId,
- discussionId,
- }, {});
+ return this.discussionResource.save(
+ {
+ mergeRequestId,
+ discussionId,
+ },
+ {},
+ );
}
unResolveAll(mergeRequestId, discussionId) {
@@ -66,10 +77,13 @@ class ResolveServiceClass {
discussion.loading = true;
- return this.discussionResource.delete({
- mergeRequestId,
- discussionId,
- }, {});
+ return this.discussionResource.delete(
+ {
+ mergeRequestId,
+ discussionId,
+ },
+ {},
+ );
}
}
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
new file mode 100644
index 00000000000..deddb61ca31
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -0,0 +1,199 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+import CompareVersions from './compare_versions.vue';
+import ChangedFiles from './changed_files.vue';
+import DiffFile from './diff_file.vue';
+import NoChanges from './no_changes.vue';
+import HiddenFilesWarning from './hidden_files_warning.vue';
+
+export default {
+ name: 'DiffsApp',
+ components: {
+ Icon,
+ LoadingIcon,
+ CompareVersions,
+ ChangedFiles,
+ DiffFile,
+ NoChanges,
+ HiddenFilesWarning,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ shouldShow: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ activeFile: '',
+ };
+ },
+ computed: {
+ ...mapState({
+ isLoading: state => state.diffs.isLoading,
+ diffFiles: state => state.diffs.diffFiles,
+ diffViewType: state => state.diffs.diffViewType,
+ mergeRequestDiffs: state => state.diffs.mergeRequestDiffs,
+ mergeRequestDiff: state => state.diffs.mergeRequestDiff,
+ latestVersionPath: state => state.diffs.latestVersionPath,
+ startVersion: state => state.diffs.startVersion,
+ commit: state => state.diffs.commit,
+ targetBranchName: state => state.diffs.targetBranchName,
+ renderOverflowWarning: state => state.diffs.renderOverflowWarning,
+ numTotalFiles: state => state.diffs.realSize,
+ numVisibleFiles: state => state.diffs.size,
+ plainDiffPath: state => state.diffs.plainDiffPath,
+ emailPatchPath: state => state.diffs.emailPatchPath,
+ }),
+ ...mapGetters(['isParallelView']),
+ targetBranch() {
+ return {
+ branchName: this.targetBranchName,
+ versionIndex: -1,
+ path: '',
+ };
+ },
+ notAllCommentsDisplayed() {
+ if (this.commit) {
+ return __('Only comments from the following commit are shown below');
+ } else if (this.startVersion) {
+ return __(
+ "Not all comments are displayed because you're comparing two versions of the diff.",
+ );
+ }
+ return __(
+ "Not all comments are displayed because you're viewing an old version of the diff.",
+ );
+ },
+ showLatestVersion() {
+ if (this.commit) {
+ return __('Show latest version of the diff');
+ }
+ return __('Show latest version');
+ },
+ },
+ watch: {
+ diffViewType() {
+ this.adjustView();
+ },
+ shouldShow() {
+ this.adjustView();
+ },
+ },
+ mounted() {
+ this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath });
+ this.fetchDiffFiles().catch(() => {
+ createFlash(__('Fetching diff files failed. Please reload the page to try again!'));
+ });
+ },
+ created() {
+ this.adjustView();
+ },
+ methods: {
+ ...mapActions(['setBaseConfig', 'fetchDiffFiles']),
+ setActive(filePath) {
+ this.activeFile = filePath;
+ },
+ unsetActive(filePath) {
+ if (this.activeFile === filePath) {
+ this.activeFile = '';
+ }
+ },
+ adjustView() {
+ if (this.shouldShow && this.isParallelView) {
+ window.mrTabs.expandViewContainer();
+ } else {
+ window.mrTabs.resetViewContainer();
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="shouldShow">
+ <div
+ v-if="isLoading"
+ class="loading"
+ >
+ <loading-icon />
+ </div>
+ <div
+ v-else
+ id="diffs"
+ :class="{ active: shouldShow }"
+ class="diffs tab-pane"
+ >
+ <compare-versions
+ v-if="!commit && mergeRequestDiffs.length > 1"
+ :merge-request-diffs="mergeRequestDiffs"
+ :merge-request-diff="mergeRequestDiff"
+ :start-version="startVersion"
+ :target-branch="targetBranch"
+ />
+
+ <hidden-files-warning
+ v-if="renderOverflowWarning"
+ :visible="numVisibleFiles"
+ :total="numTotalFiles"
+ :plain-diff-path="plainDiffPath"
+ :email-patch-path="emailPatchPath"
+ />
+
+ <div
+ v-if="commit || startVersion || (mergeRequestDiff && !mergeRequestDiff.latest)"
+ class="mr-version-controls"
+ >
+ <div class="content-block comments-disabled-notif clearfix">
+ <i class="fa fa-info-circle"></i>
+ {{ notAllCommentsDisplayed }}
+ <div class="pull-right">
+ <a
+ :href="latestVersionPath"
+ class="btn btn-sm"
+ >
+ {{ showLatestVersion }}
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <changed-files
+ :diff-files="diffFiles"
+ :active-file="activeFile"
+ />
+
+ <div
+ v-if="diffFiles.length > 0"
+ class="files"
+ >
+ <diff-file
+ v-for="file in diffFiles"
+ :key="file.newPath"
+ :file="file"
+ :current-user="currentUser"
+ @setActive="setActive(file.filePath)"
+ @unsetActive="unsetActive(file.filePath)"
+ />
+ </div>
+ <no-changes v-else />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/changed_files.vue b/app/assets/javascripts/diffs/components/changed_files.vue
new file mode 100644
index 00000000000..c5ef9fefc2f
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/changed_files.vue
@@ -0,0 +1,184 @@
+<script>
+import { mapGetters, mapActions } from 'vuex';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { pluralize } from '~/lib/utils/text_utility';
+import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
+import { contentTop } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import ChangedFilesDropdown from './changed_files_dropdown.vue';
+import changedFilesMixin from '../mixins/changed_files';
+
+export default {
+ components: {
+ Icon,
+ ChangedFilesDropdown,
+ ClipboardButton,
+ },
+ mixins: [changedFilesMixin],
+ props: {
+ activeFile: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isStuck: false,
+ maxWidth: 'auto',
+ offsetTop: 0,
+ };
+ },
+ computed: {
+ ...mapGetters(['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
+ sumAddedLines() {
+ return this.sumValues('addedLines');
+ },
+ sumRemovedLines() {
+ return this.sumValues('removedLines');
+ },
+ whitespaceVisible() {
+ return !getParameterValues('w')[0];
+ },
+ toggleWhitespaceText() {
+ if (this.whitespaceVisible) {
+ return __('Hide whitespace changes');
+ }
+ return __('Show whitespace changes');
+ },
+ toggleWhitespacePath() {
+ if (this.whitespaceVisible) {
+ return mergeUrlParams({ w: 1 }, window.location.href);
+ }
+
+ return mergeUrlParams({ w: 0 }, window.location.href);
+ },
+ top() {
+ return `${this.offsetTop}px`;
+ },
+ },
+ created() {
+ document.addEventListener('scroll', this.handleScroll);
+ this.offsetTop = contentTop();
+ },
+ beforeDestroy() {
+ document.removeEventListener('scroll', this.handleScroll);
+ },
+ methods: {
+ ...mapActions(['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
+ pluralize,
+ handleScroll() {
+ if (!this.updating) {
+ requestAnimationFrame(this.updateIsStuck);
+ this.updating = true;
+ }
+ },
+ updateIsStuck() {
+ if (!this.$refs.wrapper) {
+ return;
+ }
+
+ const scrollPosition = window.scrollY;
+
+ this.isStuck = scrollPosition + this.offsetTop >= this.$refs.placeholder.offsetTop;
+ this.updating = false;
+ },
+ sumValues(key) {
+ return this.diffFiles.reduce((total, file) => total + file[key], 0);
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <div ref="placeholder"></div>
+ <div
+ ref="wrapper"
+ :style="{ top }"
+ :class="{'is-stuck': isStuck}"
+ class="content-block oneline-block diff-files-changed diff-files-changed-merge-request
+ files-changed js-diff-files-changed"
+ >
+ <div class="files-changed-inner">
+ <div
+ class="inline-parallel-buttons d-none d-md-block"
+ >
+ <a
+ v-if="areAllFilesCollapsed"
+ class="btn btn-default"
+ @click="expandAllFiles"
+ >
+ {{ __('Expand all') }}
+ </a>
+ <a
+ :href="toggleWhitespacePath"
+ class="btn btn-default"
+ >
+ {{ toggleWhitespaceText }}
+ </a>
+ <div class="btn-group">
+ <button
+ id="inline-diff-btn"
+ :class="{ active: isInlineView }"
+ type="button"
+ class="btn js-inline-diff-button"
+ data-view-type="inline"
+ @click="setInlineDiffViewType"
+ >
+ {{ __('Inline') }}
+ </button>
+ <button
+ id="parallel-diff-btn"
+ :class="{ active: isParallelView }"
+ type="button"
+ class="btn js-parallel-diff-button"
+ data-view-type="parallel"
+ @click="setParallelDiffViewType"
+ >
+ {{ __('Side-by-side') }}
+ </button>
+ </div>
+ </div>
+
+ <div class="commit-stat-summary dropdown">
+ <changed-files-dropdown
+ :diff-files="diffFiles"
+ />
+
+ <span
+ v-show="activeFile"
+ class="prepend-left-5"
+ >
+ <strong class="prepend-right-5">
+ {{ truncatedDiffPath(activeFile) }}
+ </strong>
+ <clipboard-button
+ :text="activeFile"
+ :title="s__('Copy file name to clipboard')"
+ tooltip-placement="bottom"
+ tooltip-container="body"
+ class="btn btn-default btn-transparent btn-clipboard"
+ />
+ </span>
+
+ <span
+ v-show="!isStuck"
+ id="diff-stats"
+ class="diff-stats-additions-deletions-expanded"
+ >
+ with
+ <strong class="cgreen">
+ {{ pluralize(`${sumAddedLines} addition`, sumAddedLines) }}
+ </strong>
+ and
+ <strong class="cred">
+ {{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }}
+ </strong>
+ </span>
+ </div>
+ </div>
+ </div>
+ </span>
+</template>
diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
new file mode 100644
index 00000000000..f224b9dd246
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
@@ -0,0 +1,124 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import changedFilesMixin from '../mixins/changed_files';
+
+export default {
+ components: {
+ Icon,
+ },
+ mixins: [changedFilesMixin],
+ data() {
+ return {
+ searchText: '',
+ };
+ },
+ computed: {
+ filteredDiffFiles() {
+ return this.diffFiles.filter(file =>
+ file.filePath.toLowerCase().includes(this.searchText.toLowerCase()),
+ );
+ },
+ },
+ methods: {
+ clearSearch() {
+ this.searchText = '';
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ Showing
+ <button
+ class="diff-stats-summary-toggler"
+ data-toggle="dropdown"
+ type="button"
+ aria-expanded="false"
+ >
+ <span>
+ {{ n__('%d changed file', '%d changed files', diffFiles.length) }}
+ </span>
+ <icon
+ :size="8"
+ name="chevron-down"
+ />
+ </button>
+ <div class="dropdown-menu diff-file-changes">
+ <div class="dropdown-input">
+ <input
+ v-model="searchText"
+ type="search"
+ class="dropdown-input-field"
+ placeholder="Search files"
+ autocomplete="off"
+ />
+ <i
+ v-if="searchText.length === 0"
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-search dropdown-input-search">
+ </i>
+ <i
+ v-else
+ role="button"
+ class="fa fa-times dropdown-input-search"
+ @click="clearSearch"
+ ></i>
+ </div>
+ <ul>
+ <li
+ v-for="diffFile in filteredDiffFiles"
+ :key="diffFile.name"
+ >
+ <a
+ :href="`#${diffFile.fileHash}`"
+ :title="diffFile.newPath"
+ class="diff-changed-file"
+ >
+ <icon
+ :name="fileChangedIcon(diffFile)"
+ :size="16"
+ :class="fileChangedClass(diffFile)"
+ class="diff-file-changed-icon append-right-8"
+ />
+ <span class="diff-changed-file-content append-right-8">
+ <strong
+ v-if="diffFile.blob && diffFile.blob.name"
+ class="diff-changed-file-name"
+ >
+ {{ diffFile.blob.name }}
+ </strong>
+ <strong
+ v-else
+ class="diff-changed-blank-file-name"
+ >
+ {{ s__('Diffs|No file name available') }}
+ </strong>
+ <span class="diff-changed-file-path prepend-top-5">
+ {{ truncatedDiffPath(diffFile.blob.path) }}
+ </span>
+ </span>
+ <span class="diff-changed-stats">
+ <span class="cgreen">
+ +{{ diffFile.addedLines }}
+ </span>
+ <span class="cred">
+ -{{ diffFile.removedLines }}
+ </span>
+ </span>
+ </a>
+ </li>
+
+ <li
+ v-show="filteredDiffFiles.length === 0"
+ class="dropdown-menu-empty-item"
+ >
+ <a>
+ {{ __('No files found') }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </span>
+</template>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
new file mode 100644
index 00000000000..1c9ad8e77f1
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -0,0 +1,55 @@
+<script>
+import CompareVersionsDropdown from './compare_versions_dropdown.vue';
+
+export default {
+ components: {
+ CompareVersionsDropdown,
+ },
+ props: {
+ mergeRequestDiffs: {
+ type: Array,
+ required: true,
+ },
+ mergeRequestDiff: {
+ type: Object,
+ required: true,
+ },
+ startVersion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ targetBranch: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ comparableDiffs() {
+ return this.mergeRequestDiffs.slice(1);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="mr-version-controls">
+ <div class="mr-version-menus-container content-block">
+ Changes between
+ <compare-versions-dropdown
+ :other-versions="mergeRequestDiffs"
+ :merge-request-version="mergeRequestDiff"
+ :show-commit-count="true"
+ class="mr-version-dropdown"
+ />
+ and
+ <compare-versions-dropdown
+ :other-versions="comparableDiffs"
+ :start-version="startVersion"
+ :target-branch="targetBranch"
+ class="mr-version-compare-dropdown"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
new file mode 100644
index 00000000000..96cccb49378
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
@@ -0,0 +1,165 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import { n__, __ } from '~/locale';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ components: {
+ Icon,
+ TimeAgo,
+ },
+ props: {
+ otherVersions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ mergeRequestVersion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ startVersion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ targetBranch: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ showCommitCount: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ baseVersion() {
+ return {
+ name: 'hii',
+ versionIndex: -1,
+ };
+ },
+ targetVersions() {
+ if (this.mergeRequestVersion) {
+ return this.otherVersions;
+ }
+ return [...this.otherVersions, this.targetBranch];
+ },
+ selectedVersionName() {
+ const selectedVersion = this.startVersion || this.targetBranch || this.mergeRequestVersion;
+ return this.versionName(selectedVersion);
+ },
+ },
+ methods: {
+ commitsText(version) {
+ return n__(
+ `${version.commitsCount} commit,`,
+ `${version.commitsCount} commits,`,
+ version.commitsCount,
+ );
+ },
+ href(version) {
+ if (this.showCommitCount) {
+ return version.versionPath;
+ }
+ return version.comparePath;
+ },
+ versionName(version) {
+ if (this.isLatest(version)) {
+ return __('latest version');
+ }
+ if (this.targetBranch && (this.isBase(version) || !version)) {
+ return this.targetBranch.branchName;
+ }
+ return `version ${version.versionIndex}`;
+ },
+ isActive(version) {
+ if (!version) {
+ return false;
+ }
+
+ if (this.targetBranch) {
+ return (
+ (this.isBase(version) && !this.startVersion) ||
+ (this.startVersion && this.startVersion.versionIndex === version.versionIndex)
+ );
+ }
+
+ return version.versionIndex === this.mergeRequestVersion.versionIndex;
+ },
+ isBase(version) {
+ if (!version || !this.targetBranch) {
+ return false;
+ }
+ return version.versionIndex === -1;
+ },
+ isLatest(version) {
+ return (
+ this.mergeRequestVersion && version.versionIndex === this.targetVersions[0].versionIndex
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="dropdown inline">
+ <a
+ class="dropdown-toggle btn btn-default"
+ data-toggle="dropdown"
+ aria-expanded="false"
+ >
+ <span>
+ {{ selectedVersionName }}
+ </span>
+ <Icon
+ :size="12"
+ name="angle-down"
+ />
+ </a>
+ <div class="dropdown-menu dropdown-select dropdown-menu-selectable">
+ <div class="dropdown-content">
+ <ul>
+ <li
+ v-for="version in targetVersions"
+ :key="version.id"
+ >
+ <a
+ :class="{ 'is-active': isActive(version) }"
+ :href="href(version)"
+ >
+ <div>
+ <strong>
+ {{ versionName(version) }}
+ <template v-if="isBase(version)">
+ (base)
+ </template>
+ </strong>
+ </div>
+ <div>
+ <small class="commit-sha">
+ {{ version.truncatedCommitSha }}
+ </small>
+ </div>
+ <div>
+ <small>
+ <template v-if="showCommitCount">
+ {{ commitsText(version) }}
+ </template>
+ <time-ago
+ v-if="version.createdAt"
+ :time="version.createdAt"
+ class="js-timeago js-timeago-render"
+ />
+ </small>
+ </div>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </span>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
new file mode 100644
index 00000000000..48ba967285f
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -0,0 +1,62 @@
+<script>
+import { mapGetters, mapState } from 'vuex';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import { diffModes } from '~/ide/constants';
+import InlineDiffView from './inline_diff_view.vue';
+import ParallelDiffView from './parallel_diff_view.vue';
+
+export default {
+ components: {
+ InlineDiffView,
+ ParallelDiffView,
+ DiffViewer,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ projectPath: state => state.diffs.projectPath,
+ endpoint: state => state.diffs.endpoint,
+ }),
+ ...mapGetters(['isInlineView', 'isParallelView']),
+ diffMode() {
+ const diffModeKey = Object.keys(diffModes).find(key => this.diffFile[`${key}File`]);
+ return diffModes[diffModeKey] || diffModes.replaced;
+ },
+ isTextFile() {
+ return this.diffFile.text;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="diff-content">
+ <div class="diff-viewer">
+ <template v-if="isTextFile">
+ <inline-diff-view
+ v-if="isInlineView"
+ :diff-file="diffFile"
+ :diff-lines="diffFile.highlightedDiffLines || []"
+ />
+ <parallel-diff-view
+ v-else-if="isParallelView"
+ :diff-file="diffFile"
+ :diff-lines="diffFile.parallelDiffLines || []"
+ />
+ </template>
+ <diff-viewer
+ v-else
+ :diff-mode="diffMode"
+ :new-path="diffFile.newPath"
+ :new-sha="diffFile.diffRefs.headSha"
+ :old-path="diffFile.oldPath"
+ :old-sha="diffFile.diffRefs.baseSha"
+ :project-path="projectPath"/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
new file mode 100644
index 00000000000..39d535036f6
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -0,0 +1,39 @@
+<script>
+import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
+
+export default {
+ components: {
+ noteableDiscussion,
+ },
+ props: {
+ discussions: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="discussions.length"
+ >
+ <div
+ v-for="discussion in discussions"
+ :key="discussion.id"
+ class="discussion-notes diff-discussions"
+ >
+ <ul
+ :data-discussion-id="discussion.id"
+ class="notes"
+ >
+ <noteable-discussion
+ :discussion="discussion"
+ :render-header="false"
+ :render-diff-file="false"
+ :always-expanded="true"
+ />
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
new file mode 100644
index 00000000000..108eefdac5f
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -0,0 +1,191 @@
+<script>
+import { mapActions } from 'vuex';
+import _ from 'underscore';
+import { __, sprintf } from '~/locale';
+import createFlash from '~/flash';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import DiffFileHeader from './diff_file_header.vue';
+import DiffContent from './diff_content.vue';
+
+export default {
+ components: {
+ DiffFileHeader,
+ DiffContent,
+ LoadingIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isActive: false,
+ isLoadingCollapsedDiff: false,
+ forkMessageVisible: false,
+ };
+ },
+ computed: {
+ isDiscussionsExpanded() {
+ return true; // TODO: @fatihacet - Fix this.
+ },
+ isCollapsed() {
+ return this.file.collapsed || false;
+ },
+ viewBlobLink() {
+ return sprintf(
+ __('You can %{linkStart}view the blob%{linkEnd} instead.'),
+ {
+ linkStart: `<a href="${_.escape(this.file.viewPath)}">`,
+ linkEnd: '</a>',
+ },
+ false,
+ );
+ },
+ },
+ mounted() {
+ document.addEventListener('scroll', this.handleScroll);
+ },
+ beforeDestroy() {
+ document.removeEventListener('scroll', this.handleScroll);
+ },
+ methods: {
+ ...mapActions(['loadCollapsedDiff']),
+ handleToggle() {
+ const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file;
+
+ if (collapsed && !highlightedDiffLines && !parallelDiffLines.length) {
+ this.handleLoadCollapsedDiff();
+ } else {
+ this.file.collapsed = !this.file.collapsed;
+ }
+ },
+ handleScroll() {
+ if (!this.updating) {
+ requestAnimationFrame(this.scrollUpdate.bind(this));
+ this.updating = true;
+ }
+ },
+ scrollUpdate() {
+ const header = document.querySelector('.js-diff-files-changed');
+ if (!header) {
+ this.updating = false;
+ return;
+ }
+
+ const { top, bottom } = this.$el.getBoundingClientRect();
+ const { top: topOfFixedHeader, bottom: bottomOfFixedHeader } = header.getBoundingClientRect();
+
+ const headerOverlapsContent = top < topOfFixedHeader && bottom > bottomOfFixedHeader;
+ const fullyAboveHeader = bottom < bottomOfFixedHeader;
+ const fullyBelowHeader = top > topOfFixedHeader;
+
+ if (headerOverlapsContent && !this.isActive) {
+ this.$emit('setActive');
+ this.isActive = true;
+ } else if (this.isActive && (fullyAboveHeader || fullyBelowHeader)) {
+ this.$emit('unsetActive');
+ this.isActive = false;
+ }
+
+ this.updating = false;
+ },
+ handleLoadCollapsedDiff() {
+ this.isLoadingCollapsedDiff = true;
+
+ this.loadCollapsedDiff(this.file)
+ .then(() => {
+ this.isLoadingCollapsedDiff = false;
+ this.file.collapsed = false;
+ })
+ .catch(() => {
+ this.isLoadingCollapsedDiff = false;
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
+ },
+ showForkMessage() {
+ this.forkMessageVisible = true;
+ },
+ hideForkMessage() {
+ this.forkMessageVisible = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :id="file.fileHash"
+ class="diff-file file-holder"
+ >
+ <diff-file-header
+ :current-user="currentUser"
+ :diff-file="file"
+ :collapsible="true"
+ :expanded="!isCollapsed"
+ :discussions-expanded="isDiscussionsExpanded"
+ :add-merge-request-buttons="true"
+ class="js-file-title file-title"
+ @toggleFile="handleToggle"
+ @showForkMessage="showForkMessage"
+ />
+
+ <div
+ v-if="forkMessageVisible"
+ class="js-file-fork-suggestion-section file-fork-suggestion">
+ <span class="file-fork-suggestion-note">
+ You're not allowed to <span class="js-file-fork-suggestion-section-action">edit</span>
+ files in this project directly. Please fork this project,
+ make your changes there, and submit a merge request.
+ </span>
+ <a
+ :href="file.forkPath"
+ class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
+ >
+ Fork
+ </a>
+ <button
+ class="js-cancel-fork-suggestion-button btn btn-grouped"
+ type="button"
+ @click="hideForkMessage"
+ >
+ Cancel
+ </button>
+ </div>
+
+ <diff-content
+ v-show="!isCollapsed"
+ :class="{ hidden: isCollapsed || file.tooLarge }"
+ :diff-file="file"
+ />
+ <loading-icon
+ v-if="isLoadingCollapsedDiff"
+ class="diff-content loading"
+ />
+ <div
+ v-show="isCollapsed && !isLoadingCollapsedDiff && !file.tooLarge"
+ class="nothing-here-block diff-collapsed"
+ >
+ {{ __('This diff is collapsed.') }}
+ <a
+ class="click-to-expand js-click-to-expand"
+ href="#"
+ @click.prevent="handleToggle"
+ >
+ {{ __('Click to expand it.') }}
+ </a>
+ </div>
+ <div
+ v-if="file.tooLarge"
+ class="nothing-here-block diff-collapsed js-too-large-diff"
+ >
+ {{ __('This source diff could not be displayed because it is too large.') }}
+ <span v-html="viewBlobLink"></span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
new file mode 100644
index 00000000000..6bad389f778
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -0,0 +1,254 @@
+<script>
+import _ from 'underscore';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import Tooltip from '~/vue_shared/directives/tooltip';
+import { truncateSha } from '~/lib/utils/text_utility';
+import { __, s__, sprintf } from '~/locale';
+import EditButton from './edit_button.vue';
+
+export default {
+ components: {
+ ClipboardButton,
+ EditButton,
+ Icon,
+ },
+ directives: {
+ Tooltip,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ collapsible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ addMergeRequestButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ expanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ discussionsExpanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ blobForkSuggestion: null,
+ };
+ },
+ computed: {
+ icon() {
+ if (this.diffFile.submodule) {
+ return 'archive';
+ }
+
+ return this.diffFile.blob.icon;
+ },
+ titleLink() {
+ if (this.diffFile.submodule) {
+ return this.diffFile.submoduleTreeUrl || this.diffFile.submoduleLink;
+ }
+
+ return `#${this.diffFile.fileHash}`;
+ },
+ filePath() {
+ if (this.diffFile.submodule) {
+ return `${this.diffFile.filePath} @ ${truncateSha(this.diffFile.blob.id)}`;
+ }
+
+ if (this.diffFile.deletedFile) {
+ return sprintf(__('%{filePath} deleted'), { filePath: this.diffFile.filePath }, false);
+ }
+
+ return this.diffFile.filePath;
+ },
+ titleTag() {
+ return this.diffFile.fileHash ? 'a' : 'span';
+ },
+ isUsingLfs() {
+ return this.diffFile.storedExternally && this.diffFile.externalStorage === 'lfs';
+ },
+ collapseIcon() {
+ return this.expanded ? 'chevron-down' : 'chevron-right';
+ },
+ isDiscussionsExpanded() {
+ return this.discussionsExpanded && this.expanded;
+ },
+ viewFileButtonText() {
+ const truncatedContentSha = _.escape(truncateSha(this.diffFile.contentSha));
+ return sprintf(
+ s__('MergeRequests|View file @ %{commitId}'),
+ {
+ commitId: `<span class="commit-sha">${truncatedContentSha}</span>`,
+ },
+ false,
+ );
+ },
+ viewReplacedFileButtonText() {
+ const truncatedBaseSha = _.escape(truncateSha(this.diffFile.diffRefs.baseSha));
+ return sprintf(
+ s__('MergeRequests|View replaced file @ %{commitId}'),
+ {
+ commitId: `<span class="commit-sha">${truncatedBaseSha}</span>`,
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ handleToggle(e, checkTarget) {
+ if (!checkTarget || e.target === this.$refs.header) {
+ this.$emit('toggleFile');
+ }
+ },
+ showForkMessage() {
+ this.$emit('showForkMessage');
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ ref="header"
+ class="js-file-title file-title file-title-flex-parent"
+ @click="handleToggle($event, true)"
+ >
+ <div class="file-header-content">
+ <icon
+ v-if="collapsible"
+ :name="collapseIcon"
+ :size="16"
+ aria-hidden="true"
+ class="diff-toggle-caret"
+ @click.stop="handleToggle"
+ />
+ <a
+ ref="titleWrapper"
+ :href="titleLink"
+ >
+ <i
+ :class="`fa-${icon}`"
+ class="fa fa-fw"
+ aria-hidden="true"
+ ></i>
+ <span v-if="diffFile.renamedFile">
+ <strong
+ v-tooltip
+ :title="diffFile.oldPath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ diffFile.oldPath }}
+ </strong>
+ →
+ <strong
+ v-tooltip
+ :title="diffFile.newPath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ diffFile.newPath }}
+ </strong>
+ </span>
+
+ <strong
+ v-tooltip
+ v-else
+ :title="filePath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ filePath }}
+ </strong>
+ </a>
+
+ <clipboard-button
+ :title="__('Copy file path to clipboard')"
+ :text="diffFile.filePath"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+
+ <small
+ v-if="diffFile.modeChanged"
+ ref="fileMode"
+ >
+ {{ diffFile.aMode }} → {{ diffFile.bMode }}
+ </small>
+
+ <span
+ v-if="isUsingLfs"
+ class="label label-lfs append-right-5"
+ >
+ {{ __('LFS') }}
+ </span>
+ </div>
+
+ <div
+ v-if="!diffFile.submodule && addMergeRequestButtons"
+ class="file-actions d-none d-md-block"
+ >
+ <template
+ v-if="diffFile.blob && diffFile.blob.readableText"
+ >
+ <button
+ :class="{ active: isDiscussionsExpanded }"
+ :title="s__('MergeRequests|Toggle comments for this file')"
+ class="btn js-toggle-diff-comments"
+ type="button"
+ >
+ <icon name="comment" />
+ </button>
+
+ <edit-button
+ v-if="!diffFile.deletedFile"
+ :current-user="currentUser"
+ :edit-path="diffFile.editPath"
+ :can-modify-blob="diffFile.canModifyBlob"
+ @showForkMessage="showForkMessage"
+ />
+ </template>
+
+ <a
+ v-if="diffFile.replacedViewPath"
+ :href="diffFile.replacedViewPath"
+ class="btn view-file js-view-file"
+ v-html="viewReplacedFileButtonText"
+ >
+ </a>
+ <a
+ :href="diffFile.viewPath"
+ class="btn view-file js-view-file"
+ v-html="viewFileButtonText"
+ >
+ </a>
+
+ <a
+ v-tooltip
+ v-if="diffFile.externalUrl"
+ :href="diffFile.externalUrl"
+ :title="`View on ${diffFile.formattedExternalUrl}`"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="btn btn-file-option"
+ >
+ <icon name="external-link" />
+ </a>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
new file mode 100644
index 00000000000..7e50a0aed84
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -0,0 +1,105 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { pluralize, truncate } from '~/lib/utils/text_utility';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ UserAvatarImage,
+ },
+ props: {
+ discussions: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ discussionsExpanded() {
+ return this.discussions.every(discussion => discussion.expanded);
+ },
+ allDiscussions() {
+ return this.discussions.reduce((acc, note) => acc.concat(note.notes), []);
+ },
+ notesInGutter() {
+ return this.allDiscussions.slice(0, COUNT_OF_AVATARS_IN_GUTTER).map(n => ({
+ note: n.note,
+ author: n.author,
+ }));
+ },
+ moreCount() {
+ return this.allDiscussions.length - this.notesInGutter.length;
+ },
+ moreText() {
+ if (this.moreCount === 0) {
+ return '';
+ }
+
+ return pluralize(`${this.moreCount} more comment`, this.moreCount);
+ },
+ },
+ methods: {
+ ...mapActions(['toggleDiscussion']),
+ getTooltipText(noteData) {
+ let { note } = noteData;
+
+ if (note.length > LENGTH_OF_AVATAR_TOOLTIP) {
+ note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP);
+ }
+
+ return `${noteData.author.name}: ${note}`;
+ },
+ toggleDiscussions() {
+ this.discussions.forEach(discussion => {
+ this.toggleDiscussion({
+ discussionId: discussion.id,
+ });
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="diff-comment-avatar-holders">
+ <button
+ v-if="discussionsExpanded"
+ type="button"
+ aria-label="Show comments"
+ class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
+ @click="toggleDiscussions"
+ >
+ <icon
+ :size="12"
+ name="collapse"
+ />
+ </button>
+ <template v-else>
+ <user-avatar-image
+ v-for="note in notesInGutter"
+ :key="note.id"
+ :img-src="note.author.avatar_url"
+ :tooltip-text="getTooltipText(note)"
+ :size="19"
+ class="diff-comment-avatar js-diff-comment-avatar"
+ @click.native="toggleDiscussions"
+ />
+ <span
+ v-tooltip
+ v-if="moreText"
+ :title="moreText"
+ class="diff-comments-more-count has-tooltip js-diff-comment-avatar js-diff-comment-plus"
+ data-container="body"
+ data-placement="top"
+ role="button"
+ @click="toggleDiscussions"
+ >+{{ moreCount }}</span>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
new file mode 100644
index 00000000000..8999fd2ac96
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -0,0 +1,203 @@
+<script>
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import { mapState, mapGetters, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import DiffGutterAvatars from './diff_gutter_avatars.vue';
+import {
+ MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ OLD_NO_NEW_LINE_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ LINE_POSITION_RIGHT,
+ UNFOLD_COUNT,
+} from '../constants';
+import * as utils from '../store/utils';
+
+export default {
+ components: {
+ DiffGutterAvatars,
+ Icon,
+ },
+ props: {
+ fileHash: {
+ type: String,
+ required: true,
+ },
+ contextLinesPath: {
+ type: String,
+ required: true,
+ },
+ lineType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lineNumber: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ lineCode: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ linePosition: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ metaData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ showCommentButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isBottom: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState({
+ diffViewType: state => state.diffs.diffViewType,
+ diffFiles: state => state.diffs.diffFiles,
+ }),
+ ...mapGetters(['isLoggedIn', 'discussionsByLineCode']),
+ isMatchLine() {
+ return this.lineType === MATCH_LINE_TYPE;
+ },
+ isContextLine() {
+ return this.lineType === CONTEXT_LINE_TYPE;
+ },
+ isMetaLine() {
+ return this.lineType === OLD_NO_NEW_LINE_TYPE || this.lineType === NEW_NO_NEW_LINE_TYPE;
+ },
+ lineHref() {
+ return this.lineCode ? `#${this.lineCode}` : '#';
+ },
+ shouldShowCommentButton() {
+ return (
+ this.isLoggedIn &&
+ this.showCommentButton &&
+ !this.isMatchLine &&
+ !this.isContextLine &&
+ !this.hasDiscussions &&
+ !this.isMetaLine
+ );
+ },
+ discussions() {
+ return this.discussionsByLineCode[this.lineCode] || [];
+ },
+ hasDiscussions() {
+ return this.discussions.length > 0;
+ },
+ shouldShowAvatarsOnGutter() {
+ let render = this.hasDiscussions && this.showCommentButton;
+
+ if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) {
+ render = false;
+ }
+
+ return render;
+ },
+ },
+ methods: {
+ ...mapActions(['loadMoreLines']),
+ handleCommentButton() {
+ this.$emit('showCommentForm', { lineCode: this.lineCode });
+ },
+ handleLoadMoreLines() {
+ if (this.isRequesting) {
+ return;
+ }
+
+ this.isRequesting = true;
+ const endpoint = this.contextLinesPath;
+ const oldLineNumber = this.metaData.oldPos || 0;
+ const newLineNumber = this.metaData.newPos || 0;
+ const offset = newLineNumber - oldLineNumber;
+ const bottom = this.isBottom;
+ const { fileHash } = this;
+ const view = this.diffViewType;
+ let unfold = true;
+ let lineNumber = newLineNumber - 1;
+ let since = lineNumber - UNFOLD_COUNT;
+ let to = lineNumber;
+
+ if (bottom) {
+ lineNumber = newLineNumber + 1;
+ since = lineNumber;
+ to = lineNumber + UNFOLD_COUNT;
+ } else {
+ const diffFile = utils.findDiffFile(this.diffFiles, this.fileHash);
+ const indexForInline = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, {
+ oldLineNumber,
+ newLineNumber,
+ });
+ const prevLine = diffFile.highlightedDiffLines[indexForInline - 2];
+ const prevLineNumber = (prevLine && prevLine.newLine) || 0;
+
+ if (since <= prevLineNumber + 1) {
+ since = prevLineNumber + 1;
+ unfold = false;
+ }
+ }
+
+ const params = { since, to, bottom, offset, unfold, view };
+ const lineNumbers = { oldLineNumber, newLineNumber };
+ this.loadMoreLines({ endpoint, params, lineNumbers, fileHash })
+ .then(() => {
+ this.isRequesting = false;
+ })
+ .catch(() => {
+ createFlash(s__('Diffs|Something went wrong while fetching diff lines.'));
+ this.isRequesting = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <span
+ v-if="isMatchLine"
+ class="context-cell"
+ role="button"
+ @click="handleLoadMoreLines"
+ >...</span>
+ <template
+ v-else
+ >
+ <button
+ v-show="shouldShowCommentButton"
+ type="button"
+ class="add-diff-note js-add-diff-note-button"
+ title="Add a comment to this line"
+ @click="handleCommentButton"
+ >
+ <icon
+ :size="12"
+ name="comment"
+ />
+ </button>
+ <a
+ v-if="lineNumber"
+ :data-linenumber="lineNumber"
+ :href="lineHref"
+ >
+ </a>
+ <diff-gutter-avatars
+ v-if="shouldShowAvatarsOnGutter"
+ :discussions="discussions"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
new file mode 100644
index 00000000000..6943b462e86
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -0,0 +1,114 @@
+<script>
+import $ from 'jquery';
+import { mapState, mapGetters, mapActions } from 'vuex';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import noteForm from '../../notes/components/note_form.vue';
+import { getNoteFormData } from '../store/utils';
+import Autosave from '../../autosave';
+import { DIFF_NOTE_TYPE, NOTE_TYPE } from '../constants';
+
+export default {
+ components: {
+ noteForm,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ line: {
+ type: Object,
+ required: true,
+ },
+ position: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ noteTargetLine: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ noteableData: state => state.notes.noteableData,
+ diffViewType: state => state.diffs.diffViewType,
+ }),
+ ...mapGetters(['isLoggedIn', 'noteableType', 'getNoteableData', 'getNotesDataByProp']),
+ },
+ mounted() {
+ if (this.isLoggedIn) {
+ const noteableData = this.getNoteableData;
+ const keys = [
+ NOTE_TYPE,
+ this.noteableType,
+ noteableData.id,
+ noteableData.diff_head_sha,
+ DIFF_NOTE_TYPE,
+ noteableData.source_project_id,
+ this.line.lineCode,
+ ];
+
+ this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys);
+ }
+ },
+ methods: {
+ ...mapActions(['cancelCommentForm', 'saveNote', 'fetchDiscussions']),
+ handleCancelCommentForm() {
+ this.autosave.reset();
+ this.cancelCommentForm({
+ lineCode: this.line.lineCode,
+ });
+ },
+ handleSaveNote(note) {
+ const postData = getNoteFormData({
+ note,
+ noteableData: this.noteableData,
+ noteableType: this.noteableType,
+ noteTargetLine: this.noteTargetLine,
+ diffViewType: this.diffViewType,
+ diffFile: this.diffFile,
+ linePosition: this.position,
+ });
+
+ this.saveNote(postData)
+ .then(() => {
+ const endpoint = this.getNotesDataByProp('discussionsPath');
+
+ this.fetchDiscussions(endpoint)
+ .then(() => {
+ this.handleCancelCommentForm();
+ })
+ .catch(() => {
+ createFlash(s__('MergeRequests|Updating discussions failed'));
+ });
+ })
+ .catch(() => {
+ createFlash(s__('MergeRequests|Saving the comment failed'));
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="content discussion-form discussion-form-container discussion-notes"
+ >
+ <note-form
+ ref="noteForm"
+ :is-editing="true"
+ :line-code="line.lineCode"
+ save-button-title="Comment"
+ class="diff-comment-form"
+ @cancelForm="handleCancelCommentForm"
+ @handleFormUpdate="handleSaveNote"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue
new file mode 100644
index 00000000000..ebf90631d76
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/edit_button.vue
@@ -0,0 +1,42 @@
+<script>
+export default {
+ props: {
+ editPath: {
+ type: String,
+ required: true,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ canModifyBlob: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ handleEditClick(evt) {
+ if (!this.currentUser || this.canModifyBlob) {
+ // if we can Edit, do default Edit button behavior
+ return;
+ }
+
+ if (this.currentUser.canFork && this.currentUser.canCreateMergeRequest) {
+ evt.preventDefault();
+ this.$emit('showForkMessage');
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <a
+ :href="editPath"
+ class="btn btn-default js-edit-blob"
+ @click="handleEditClick"
+ >
+ Edit
+ </a>
+</template>
diff --git a/app/assets/javascripts/diffs/components/hidden_files_warning.vue b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
new file mode 100644
index 00000000000..017dcfcc357
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
@@ -0,0 +1,51 @@
+<script>
+export default {
+ props: {
+ total: {
+ type: String,
+ required: true,
+ },
+ visible: {
+ type: Number,
+ required: true,
+ },
+ plainDiffPath: {
+ type: String,
+ required: true,
+ },
+ emailPatchPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="alert alert-warning">
+ <h4>
+ {{ __('Too many changes to show.') }}
+ <div class="pull-right">
+ <a
+ :href="plainDiffPath"
+ class="btn btn-sm"
+ >
+ {{ __('Plain diff') }}
+ </a>
+ <a
+ :href="emailPatchPath"
+ class="btn btn-sm"
+ >
+ {{ __('Email patch') }}
+ </a>
+ </div>
+ </h4>
+ <p>
+ To preserve performance only
+ <strong>
+ {{ visible }} of {{ total }}
+ </strong>
+ files are displayed.
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
new file mode 100644
index 00000000000..21376117bef
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -0,0 +1,117 @@
+<script>
+import diffContentMixin from '../mixins/diff_content';
+import {
+ MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ OLD_NO_NEW_LINE_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ LINE_HOVER_CLASS_NAME,
+ LINE_UNFOLD_CLASS_NAME,
+} from '../constants';
+
+export default {
+ mixins: [diffContentMixin],
+ methods: {
+ handleMouse(lineCode, isOver) {
+ this.hoveredLineCode = isOver ? lineCode : null;
+ },
+ getLineClass(line) {
+ const isSameLine = this.hoveredLineCode && this.hoveredLineCode === line.lineCode;
+ const isMatchLine = line.type === MATCH_LINE_TYPE;
+ const isContextLine = line.type === CONTEXT_LINE_TYPE;
+ const isMetaLine = line.type === OLD_NO_NEW_LINE_TYPE || line.type === NEW_NO_NEW_LINE_TYPE;
+
+ return {
+ [line.type]: line.type,
+ [LINE_UNFOLD_CLASS_NAME]: isMatchLine,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn && isSameLine && !isMatchLine && !isContextLine && !isMetaLine,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <table
+ :class="userColorScheme"
+ :data-commit-id="commitId"
+ class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view">
+ <tbody>
+ <template
+ v-for="(line, index) in normalizedDiffLines"
+ >
+ <tr
+ :id="line.lineCode || `${fileHash}_${line.oldLine}_${line.newLine}`"
+ :key="line.lineCode"
+ :class="getRowClass(line)"
+ class="line_holder"
+ @mouseover="handleMouse(line.lineCode, true)"
+ @mouseout="handleMouse(line.lineCode, false)"
+ >
+ <td
+ :class="getLineClass(line)"
+ class="diff-line-num old_line"
+ >
+ <diff-line-gutter-content
+ :file-hash="fileHash"
+ :line-type="line.type"
+ :line-code="line.lineCode"
+ :line-number="line.oldLine"
+ :meta-data="line.metaData"
+ :show-comment-button="true"
+ :context-lines-path="diffFile.contextLinesPath"
+ :is-bottom="index + 1 === diffLinesLength"
+ @showCommentForm="handleShowCommentForm"
+ />
+ </td>
+ <td
+ :class="getLineClass(line)"
+ class="diff-line-num new_line"
+ >
+ <diff-line-gutter-content
+ :file-hash="fileHash"
+ :line-type="line.type"
+ :line-code="line.lineCode"
+ :line-number="line.newLine"
+ :meta-data="line.metaData"
+ :is-bottom="index + 1 === diffLinesLength"
+ :context-lines-path="diffFile.contextLinesPath"
+ />
+ </td>
+ <td
+ :class="line.type"
+ class="line_content"
+ v-html="line.richText"
+ >
+ </td>
+ </tr>
+ <tr
+ v-if="isDiscussionExpanded(line.lineCode) || diffLineCommentForms[line.lineCode]"
+ :key="index"
+ :class="discussionsByLineCode[line.lineCode] ? '' : 'js-temp-notes-holder'"
+ class="notes_holder"
+ >
+ <td
+ class="notes_line"
+ colspan="2"
+ ></td>
+ <td class="notes_content">
+ <div class="content">
+ <diff-discussions
+ :discussions="discussionsByLineCode[line.lineCode] || []"
+ />
+ <diff-line-note-form
+ v-if="diffLineCommentForms[line.lineCode]"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line"
+ :note-target-line="diffLines[index]"
+ />
+ </div>
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+</template>
diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue
new file mode 100644
index 00000000000..d817157fbcd
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/no_changes.vue
@@ -0,0 +1,49 @@
+<script>
+import { mapState } from 'vuex';
+import emptyImage from '~/../../views/shared/icons/_mr_widget_empty_state.svg';
+
+export default {
+ data() {
+ return {
+ emptyImage,
+ };
+ },
+ computed: {
+ ...mapState({
+ sourceBranch: state => state.notes.noteableData.source_branch,
+ targetBranch: state => state.notes.noteableData.target_branch,
+ newBlobPath: state => state.notes.noteableData.new_blob_path,
+ }),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="row empty-state nothing-here-block"
+ >
+ <div class="col-xs-12">
+ <div class="svg-content">
+ <span
+ v-html="emptyImage"
+ ></span>
+ </div>
+ </div>
+ <div class="col-xs-12">
+ <div class="text-content text-center">
+ No changes between
+ <span class="ref-name">{{ sourceBranch }}</span>
+ and
+ <span class="ref-name">{{ targetBranch }}</span>
+ <div class="text-center">
+ <a
+ :href="newBlobPath"
+ class="btn btn-save"
+ >
+ {{ __('Create commit') }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
new file mode 100644
index 00000000000..60edbcbbda8
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -0,0 +1,224 @@
+<script>
+import diffContentMixin from '../mixins/diff_content';
+import {
+ EMPTY_CELL_TYPE,
+ MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ OLD_NO_NEW_LINE_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ LINE_HOVER_CLASS_NAME,
+ LINE_UNFOLD_CLASS_NAME,
+ LINE_POSITION_RIGHT,
+} from '../constants';
+
+export default {
+ mixins: [diffContentMixin],
+ computed: {
+ parallelDiffLines() {
+ return this.normalizedDiffLines.map(line => {
+ if (!line.left) {
+ Object.assign(line, { left: { type: EMPTY_CELL_TYPE } });
+ } else if (!line.right) {
+ Object.assign(line, { right: { type: EMPTY_CELL_TYPE } });
+ }
+
+ return line;
+ });
+ },
+ },
+ methods: {
+ hasDiscussion(line) {
+ const discussions = this.discussionsByLineCode;
+ const hasDiscussion = discussions[line.left.lineCode] || discussions[line.right.lineCode];
+
+ return hasDiscussion;
+ },
+ getClassName(line, position) {
+ const { type, lineCode } = line[position];
+ const isMatchLine = type === MATCH_LINE_TYPE;
+ const isContextLine = !isMatchLine && type !== EMPTY_CELL_TYPE && type !== CONTEXT_LINE_TYPE;
+ const isMetaLine = type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE;
+ const isSameLine = this.hoveredLineCode && this.hoveredLineCode === lineCode;
+ const isSameSection = position === this.hoveredSection;
+
+ return {
+ [type]: type,
+ [LINE_UNFOLD_CLASS_NAME]: isMatchLine,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn && isContextLine && isSameLine && isSameSection && !isMetaLine,
+ };
+ },
+ handleMouse(e, line, isHover) {
+ if (isHover) {
+ const cell = e.target.closest('td');
+
+ if (this.$refs.leftLines.indexOf(cell) > -1) {
+ this.hoveredLineCode = line.left.lineCode;
+ this.hoveredSection = 'left';
+ } else if (this.$refs.rightLines.indexOf(cell) > -1) {
+ this.hoveredLineCode = line.right.lineCode;
+ this.hoveredSection = 'right';
+ }
+ } else {
+ this.hoveredLineCode = null;
+ this.hoveredSection = null;
+ }
+ },
+ shouldRenderDiscussionsRow(line) {
+ const hasDiscussion = this.hasDiscussion(line) && this.hasAnyExpandedDiscussion(line);
+ const hasCommentFormOnLeft = this.diffLineCommentForms[line.left.lineCode];
+ const hasCommentFormOnRight = this.diffLineCommentForms[line.right.lineCode];
+
+ return hasDiscussion || hasCommentFormOnLeft || hasCommentFormOnRight;
+ },
+ shouldRenderDiscussions(line, position) {
+ const { lineCode } = line[position];
+ let render = this.discussionsByLineCode[lineCode] && this.isDiscussionExpanded(lineCode);
+
+ // Avoid rendering context line discussions on the right side in parallel view
+ if (position === LINE_POSITION_RIGHT) {
+ render = render && line.right.type;
+ }
+
+ return render;
+ },
+ hasAnyExpandedDiscussion(line) {
+ const isLeftExpanded = this.isDiscussionExpanded(line.left.lineCode);
+ const isRightExpanded = this.isDiscussionExpanded(line.right.lineCode);
+
+ return isLeftExpanded || isRightExpanded;
+ },
+ getLineCode(line, side) {
+ const { lineCode } = side;
+ if (lineCode) {
+ return lineCode;
+ }
+
+ return `${this.fileHash}_${line.left.oldLine}_${line.right.newLine}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :class="userColorScheme"
+ :data-commit-id="commitId"
+ class="code diff-wrap-lines js-syntax-highlight text-file">
+ <table>
+ <tbody>
+ <template
+ v-for="(line, index) in parallelDiffLines"
+ >
+ <tr
+ :key="index"
+ :class="getRowClass(line)"
+ class="line_holder parallel"
+ @mouseover="handleMouse($event, line, true)"
+ @mouseout="handleMouse($event, line, false)"
+ >
+ <td
+ ref="leftLines"
+ :class="getClassName(line, 'left')"
+ class="diff-line-num old_line"
+ >
+ <diff-line-gutter-content
+ :file-hash="fileHash"
+ :line-type="line.left.type"
+ :line-code="line.left.lineCode"
+ :line-number="line.left.oldLine"
+ :meta-data="line.left.metaData"
+ :show-comment-button="true"
+ :context-lines-path="diffFile.contextLinesPath"
+ :is-bottom="index + 1 === diffLinesLength"
+ line-position="left"
+ @showCommentForm="handleShowCommentForm"
+ />
+ </td>
+ <td
+ ref="leftLines"
+ :class="getClassName(line, 'left')"
+ :id="getLineCode(line, line.left)"
+ class="line_content parallel left-side"
+ v-html="line.left.richText"
+ >
+ </td>
+ <td
+ ref="rightLines"
+ :class="getClassName(line, 'right')"
+ class="diff-line-num new_line"
+ >
+ <diff-line-gutter-content
+ :file-hash="fileHash"
+ :line-type="line.right.type"
+ :line-code="line.right.lineCode"
+ :line-number="line.right.newLine"
+ :meta-data="line.right.metaData"
+ :show-comment-button="true"
+ :context-lines-path="diffFile.contextLinesPath"
+ :is-bottom="index + 1 === diffLinesLength"
+ line-position="right"
+ @showCommentForm="handleShowCommentForm"
+ />
+ </td>
+ <td
+ ref="rightLines"
+ :class="getClassName(line, 'right')"
+ :id="getLineCode(line, line.right)"
+ class="line_content parallel right-side"
+ v-html="line.right.richText"
+ >
+ </td>
+ </tr>
+ <tr
+ v-if="shouldRenderDiscussionsRow(line)"
+ :key="line.left.lineCode || line.right.lineCode"
+ :class="hasDiscussion(line) ? '' : 'js-temp-notes-holder'"
+ class="notes_holder"
+ >
+ <td class="notes_line old"></td>
+ <td class="notes_content parallel old">
+ <div
+ v-if="shouldRenderDiscussions(line, 'left')"
+ class="content"
+ >
+ <diff-discussions
+ :discussions="discussionsByLineCode[line.left.lineCode]"
+ />
+ </div>
+ <diff-line-note-form
+ v-if="diffLineCommentForms[line.left.lineCode] &&
+ diffLineCommentForms[line.left.lineCode]"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line.left"
+ :note-target-line="diffLines[index].left"
+ position="left"
+ />
+ </td>
+ <td class="notes_line new"></td>
+ <td class="notes_content parallel new">
+ <div
+ v-if="shouldRenderDiscussions(line, 'right')"
+ class="content"
+ >
+ <diff-discussions
+ :discussions="discussionsByLineCode[line.right.lineCode]"
+ />
+ </div>
+ <diff-line-note-form
+ v-if="diffLineCommentForms[line.right.lineCode] &&
+ diffLineCommentForms[line.right.lineCode] && line.right.type"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line.right"
+ :note-target-line="diffLines[index].right"
+ position="right"
+ />
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
new file mode 100644
index 00000000000..d314f08e60e
--- /dev/null
+++ b/app/assets/javascripts/diffs/constants.js
@@ -0,0 +1,25 @@
+export const INLINE_DIFF_VIEW_TYPE = 'inline';
+export const PARALLEL_DIFF_VIEW_TYPE = 'parallel';
+export const MATCH_LINE_TYPE = 'match';
+export const OLD_NO_NEW_LINE_TYPE = 'old-nonewline';
+export const NEW_NO_NEW_LINE_TYPE = 'new-nonewline';
+export const CONTEXT_LINE_TYPE = 'context';
+export const EMPTY_CELL_TYPE = 'empty-cell';
+export const COMMENT_FORM_TYPE = 'commentForm';
+export const DIFF_NOTE_TYPE = 'DiffNote';
+export const NOTE_TYPE = 'Note';
+export const NEW_LINE_TYPE = 'new';
+export const OLD_LINE_TYPE = 'old';
+export const TEXT_DIFF_POSITION_TYPE = 'text';
+
+export const LINE_POSITION_LEFT = 'left';
+export const LINE_POSITION_RIGHT = 'right';
+
+export const DIFF_VIEW_COOKIE_NAME = 'diff_view';
+export const LINE_HOVER_CLASS_NAME = 'is-over';
+export const LINE_UNFOLD_CLASS_NAME = 'unfold js-unfold';
+export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded';
+
+export const UNFOLD_COUNT = 20;
+export const COUNT_OF_AVATARS_IN_GUTTER = 3;
+export const LENGTH_OF_AVATAR_TOOLTIP = 17;
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
new file mode 100644
index 00000000000..aae89109c27
--- /dev/null
+++ b/app/assets/javascripts/diffs/index.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import diffsApp from './components/app.vue';
+
+export default function initDiffsApp(store) {
+ return new Vue({
+ el: '#js-diffs-app',
+ name: 'MergeRequestDiffs',
+ components: {
+ diffsApp,
+ },
+ store,
+ data() {
+ const { dataset } = document.querySelector(this.$options.el);
+
+ return {
+ endpoint: dataset.endpoint,
+ projectPath: dataset.projectPath,
+ currentUser: convertObjectPropsToCamelCase(JSON.parse(dataset.currentUserData), {
+ deep: true,
+ }),
+ };
+ },
+ computed: {
+ ...mapState({
+ activeTab: state => state.page.activeTab,
+ }),
+ },
+ render(createElement) {
+ return createElement('diffs-app', {
+ props: {
+ endpoint: this.endpoint,
+ currentUser: this.currentUser,
+ projectPath: this.projectPath,
+ shouldShow: this.activeTab === 'diffs',
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/diffs/mixins/changed_files.js b/app/assets/javascripts/diffs/mixins/changed_files.js
new file mode 100644
index 00000000000..da1339f0ffa
--- /dev/null
+++ b/app/assets/javascripts/diffs/mixins/changed_files.js
@@ -0,0 +1,38 @@
+export default {
+ props: {
+ diffFiles: {
+ type: Array,
+ required: true,
+ },
+ },
+ methods: {
+ fileChangedIcon(diffFile) {
+ if (diffFile.deletedFile) {
+ return 'file-deletion';
+ } else if (diffFile.newFile) {
+ return 'file-addition';
+ }
+ return 'file-modified';
+ },
+ fileChangedClass(diffFile) {
+ if (diffFile.deletedFile) {
+ return 'cred';
+ } else if (diffFile.newFile) {
+ return 'cgreen';
+ }
+
+ return '';
+ },
+ truncatedDiffPath(path) {
+ const maxLength = 60;
+
+ if (path.length > maxLength) {
+ const start = path.length - maxLength;
+ const end = start + maxLength;
+ return `...${path.slice(start, end)}`;
+ }
+
+ return path;
+ },
+ },
+};
diff --git a/app/assets/javascripts/diffs/mixins/diff_content.js b/app/assets/javascripts/diffs/mixins/diff_content.js
new file mode 100644
index 00000000000..bef06ad2b52
--- /dev/null
+++ b/app/assets/javascripts/diffs/mixins/diff_content.js
@@ -0,0 +1,89 @@
+import { mapState, mapGetters, mapActions } from 'vuex';
+import diffDiscussions from '../components/diff_discussions.vue';
+import diffLineGutterContent from '../components/diff_line_gutter_content.vue';
+import diffLineNoteForm from '../components/diff_line_note_form.vue';
+import { trimFirstCharOfLineContent } from '../store/utils';
+import { CONTEXT_LINE_TYPE, CONTEXT_LINE_CLASS_NAME } from '../constants';
+
+export default {
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ hoveredLineCode: null,
+ hoveredSection: null,
+ };
+ },
+ components: {
+ diffDiscussions,
+ diffLineNoteForm,
+ diffLineGutterContent,
+ },
+ computed: {
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
+ ...mapGetters(['discussionsByLineCode', 'isLoggedIn', 'commit']),
+ commitId() {
+ return this.commit && this.commit.id;
+ },
+ userColorScheme() {
+ return window.gon.user_color_scheme;
+ },
+ normalizedDiffLines() {
+ return this.diffLines.map(line => {
+ if (line.richText) {
+ return this.trimFirstChar(line);
+ }
+
+ if (line.left) {
+ Object.assign(line, { left: this.trimFirstChar(line.left) });
+ }
+
+ if (line.right) {
+ Object.assign(line, { right: this.trimFirstChar(line.right) });
+ }
+
+ return line;
+ });
+ },
+ diffLinesLength() {
+ return this.normalizedDiffLines.length;
+ },
+ fileHash() {
+ return this.diffFile.fileHash;
+ },
+ },
+ methods: {
+ ...mapActions(['showCommentForm', 'cancelCommentForm']),
+ getRowClass(line) {
+ const isContextLine = line.left
+ ? line.left.type === CONTEXT_LINE_TYPE
+ : line.type === CONTEXT_LINE_TYPE;
+
+ return {
+ [line.type]: line.type,
+ [CONTEXT_LINE_CLASS_NAME]: isContextLine,
+ };
+ },
+ trimFirstChar(line) {
+ return trimFirstCharOfLineContent(line);
+ },
+ handleShowCommentForm(params) {
+ this.showCommentForm({ lineCode: params.lineCode });
+ },
+ isDiscussionExpanded(lineCode) {
+ const discussions = this.discussionsByLineCode[lineCode];
+
+ return discussions ? discussions.every(discussion => discussion.expanded) : false;
+ },
+ },
+};
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
new file mode 100644
index 00000000000..bf188a44022
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
+import Cookies from 'js-cookie';
+import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import * as types from './mutation_types';
+import {
+ PARALLEL_DIFF_VIEW_TYPE,
+ INLINE_DIFF_VIEW_TYPE,
+ DIFF_VIEW_COOKIE_NAME,
+} from '../constants';
+
+export const setBaseConfig = ({ commit }, options) => {
+ const { endpoint, projectPath } = options;
+ commit(types.SET_BASE_CONFIG, { endpoint, projectPath });
+};
+
+export const setLoadingState = ({ commit }, state) => {
+ commit(types.SET_LOADING, state);
+};
+
+export const fetchDiffFiles = ({ state, commit }) => {
+ commit(types.SET_LOADING, true);
+
+ return axios
+ .get(state.endpoint)
+ .then(res => {
+ commit(types.SET_LOADING, false);
+ commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
+ commit(types.SET_DIFF_DATA, res.data);
+ return Vue.nextTick();
+ })
+ .then(handleLocationHash);
+};
+
+export const setInlineDiffViewType = ({ commit }) => {
+ commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE);
+
+ Cookies.set(DIFF_VIEW_COOKIE_NAME, INLINE_DIFF_VIEW_TYPE);
+ const url = mergeUrlParams({ view: INLINE_DIFF_VIEW_TYPE }, window.location.href);
+ historyPushState(url);
+};
+
+export const setParallelDiffViewType = ({ commit }) => {
+ commit(types.SET_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE);
+
+ Cookies.set(DIFF_VIEW_COOKIE_NAME, PARALLEL_DIFF_VIEW_TYPE);
+ const url = mergeUrlParams({ view: PARALLEL_DIFF_VIEW_TYPE }, window.location.href);
+ historyPushState(url);
+};
+
+export const showCommentForm = ({ commit }, params) => {
+ commit(types.ADD_COMMENT_FORM_LINE, params);
+};
+
+export const cancelCommentForm = ({ commit }, params) => {
+ commit(types.REMOVE_COMMENT_FORM_LINE, params);
+};
+
+export const loadMoreLines = ({ commit }, options) => {
+ const { endpoint, params, lineNumbers, fileHash } = options;
+
+ params.from_merge_request = true;
+
+ return axios.get(endpoint, { params }).then(res => {
+ const contextLines = res.data || [];
+
+ commit(types.ADD_CONTEXT_LINES, {
+ lineNumbers,
+ contextLines,
+ params,
+ fileHash,
+ });
+ });
+};
+
+export const loadCollapsedDiff = ({ commit }, file) =>
+ axios.get(file.loadCollapsedDiffUrl).then(res => {
+ commit(types.ADD_COLLAPSED_DIFFS, {
+ file,
+ data: res.data,
+ });
+ });
+
+export const expandAllFiles = ({ commit }) => {
+ commit(types.EXPAND_ALL_FILES);
+};
+
+export default {
+ setBaseConfig,
+ setLoadingState,
+ fetchDiffFiles,
+ setInlineDiffViewType,
+ setParallelDiffViewType,
+ showCommentForm,
+ cancelCommentForm,
+ loadMoreLines,
+ loadCollapsedDiff,
+ expandAllFiles,
+};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
new file mode 100644
index 00000000000..66d0f47d102
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -0,0 +1,16 @@
+import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
+
+export default {
+ isParallelView(state) {
+ return state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
+ },
+ isInlineView(state) {
+ return state.diffViewType === INLINE_DIFF_VIEW_TYPE;
+ },
+ areAllFilesCollapsed(state) {
+ return state.diffFiles.every(file => file.collapsed);
+ },
+ commit(state) {
+ return state.commit;
+ },
+};
diff --git a/app/assets/javascripts/diffs/store/index.js b/app/assets/javascripts/diffs/store/index.js
new file mode 100644
index 00000000000..e6aa8f5b12a
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/index.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import diffsModule from './modules';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ modules: {
+ diffs: diffsModule,
+ },
+});
diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js
new file mode 100644
index 00000000000..94caa131506
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/modules/index.js
@@ -0,0 +1,26 @@
+import Cookies from 'js-cookie';
+import { getParameterValues } from '~/lib/utils/url_utility';
+import actions from '../actions';
+import getters from '../getters';
+import mutations from '../mutations';
+import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
+
+const viewTypeFromQueryString = getParameterValues('view')[0];
+const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
+const defaultViewType = INLINE_DIFF_VIEW_TYPE;
+
+export default {
+ state: {
+ isLoading: true,
+ endpoint: '',
+ basePath: '',
+ commit: null,
+ diffFiles: [],
+ mergeRequestDiffs: [],
+ diffLineCommentForms: {},
+ diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
+ },
+ getters,
+ actions,
+ mutations,
+};
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
new file mode 100644
index 00000000000..63e9239dce4
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -0,0 +1,11 @@
+export const SET_BASE_CONFIG = 'SET_BASE_CONFIG';
+export const SET_LOADING = 'SET_LOADING';
+export const SET_DIFF_DATA = 'SET_DIFF_DATA';
+export const SET_DIFF_FILES = 'SET_DIFF_FILES';
+export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE';
+export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS';
+export const ADD_COMMENT_FORM_LINE = 'ADD_COMMENT_FORM_LINE';
+export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE';
+export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
+export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
+export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
new file mode 100644
index 00000000000..339a33f8b71
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -0,0 +1,86 @@
+import Vue from 'vue';
+import _ from 'underscore';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils';
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_BASE_CONFIG](state, options) {
+ const { endpoint, projectPath } = options;
+ Object.assign(state, { endpoint, projectPath });
+ },
+
+ [types.SET_LOADING](state, isLoading) {
+ Object.assign(state, { isLoading });
+ },
+
+ [types.SET_DIFF_DATA](state, data) {
+ Object.assign(state, {
+ ...convertObjectPropsToCamelCase(data, { deep: true }),
+ });
+ },
+
+ [types.SET_DIFF_FILES](state, diffFiles) {
+ Object.assign(state, {
+ diffFiles: convertObjectPropsToCamelCase(diffFiles, { deep: true }),
+ });
+ },
+
+ [types.SET_MERGE_REQUEST_DIFFS](state, mergeRequestDiffs) {
+ Object.assign(state, {
+ mergeRequestDiffs: convertObjectPropsToCamelCase(mergeRequestDiffs, { deep: true }),
+ });
+ },
+
+ [types.SET_DIFF_VIEW_TYPE](state, diffViewType) {
+ Object.assign(state, { diffViewType });
+ },
+
+ [types.ADD_COMMENT_FORM_LINE](state, { lineCode }) {
+ Vue.set(state.diffLineCommentForms, lineCode, true);
+ },
+
+ [types.REMOVE_COMMENT_FORM_LINE](state, { lineCode }) {
+ Vue.delete(state.diffLineCommentForms, lineCode);
+ },
+
+ [types.ADD_CONTEXT_LINES](state, options) {
+ const { lineNumbers, contextLines, fileHash } = options;
+ const { bottom } = options.params;
+ const diffFile = findDiffFile(state.diffFiles, fileHash);
+ const { highlightedDiffLines, parallelDiffLines } = diffFile;
+
+ removeMatchLine(diffFile, lineNumbers, bottom);
+ const lines = addLineReferences(contextLines, lineNumbers, bottom);
+ addContextLines({
+ inlineLines: highlightedDiffLines,
+ parallelLines: parallelDiffLines,
+ contextLines: lines,
+ bottom,
+ lineNumbers,
+ });
+ },
+
+ [types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
+ const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
+ const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
+
+ if (newFileData) {
+ const index = _.findIndex(state.diffFiles, f => f.fileHash === file.fileHash);
+ state.diffFiles.splice(index, 1, newFileData);
+ }
+ },
+
+ [types.EXPAND_ALL_FILES](state) {
+ const diffFiles = [];
+
+ state.diffFiles.forEach(file => {
+ diffFiles.push({
+ ...file,
+ collapsed: false,
+ });
+ });
+
+ Object.assign(state, { diffFiles });
+ },
+};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
new file mode 100644
index 00000000000..da7ae16aaf1
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -0,0 +1,172 @@
+import _ from 'underscore';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import {
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+ TEXT_DIFF_POSITION_TYPE,
+ DIFF_NOTE_TYPE,
+ NEW_LINE_TYPE,
+ OLD_LINE_TYPE,
+ MATCH_LINE_TYPE,
+} from '../constants';
+
+export function findDiffFile(files, hash) {
+ return files.filter(file => file.fileHash === hash)[0];
+}
+
+export const getReversePosition = linePosition => {
+ if (linePosition === LINE_POSITION_RIGHT) {
+ return LINE_POSITION_LEFT;
+ }
+
+ return LINE_POSITION_RIGHT;
+};
+
+export function getNoteFormData(params) {
+ const {
+ note,
+ noteableType,
+ noteableData,
+ diffFile,
+ noteTargetLine,
+ diffViewType,
+ linePosition,
+ } = params;
+
+ const position = JSON.stringify({
+ base_sha: diffFile.diffRefs.baseSha,
+ start_sha: diffFile.diffRefs.startSha,
+ head_sha: diffFile.diffRefs.headSha,
+ old_path: diffFile.oldPath,
+ new_path: diffFile.newPath,
+ position_type: TEXT_DIFF_POSITION_TYPE,
+ old_line: noteTargetLine.oldLine,
+ new_line: noteTargetLine.newLine,
+ });
+
+ const postData = {
+ view: diffViewType,
+ line_type: linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE,
+ merge_request_diff_head_sha: diffFile.diffRefs.headSha,
+ in_reply_to_discussion_id: '',
+ note_project_id: '',
+ target_type: noteableData.targetType,
+ target_id: noteableData.id,
+ note: {
+ note,
+ position,
+ noteable_type: noteableType,
+ noteable_id: noteableData.id,
+ commit_id: '',
+ type: DIFF_NOTE_TYPE,
+ line_code: noteTargetLine.lineCode,
+ },
+ };
+
+ return {
+ endpoint: noteableData.create_note_path,
+ data: postData,
+ };
+}
+
+export const findIndexInInlineLines = (lines, lineNumbers) => {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+
+ return _.findIndex(
+ lines,
+ line => line.oldLine === oldLineNumber && line.newLine === newLineNumber,
+ );
+};
+
+export const findIndexInParallelLines = (lines, lineNumbers) => {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+
+ return _.findIndex(
+ lines,
+ line =>
+ line.left &&
+ line.right &&
+ line.left.oldLine === oldLineNumber &&
+ line.right.newLine === newLineNumber,
+ );
+};
+
+export function removeMatchLine(diffFile, lineNumbers, bottom) {
+ const indexForInline = findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
+ const indexForParallel = findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
+ const factor = bottom ? 1 : -1;
+
+ diffFile.highlightedDiffLines.splice(indexForInline + factor, 1);
+ diffFile.parallelDiffLines.splice(indexForParallel + factor, 1);
+}
+
+export function addLineReferences(lines, lineNumbers, bottom) {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+ const lineCount = lines.length;
+ let matchLineIndex = -1;
+
+ const linesWithNumbers = lines.map((l, index) => {
+ const line = convertObjectPropsToCamelCase(l);
+
+ if (line.type === MATCH_LINE_TYPE) {
+ matchLineIndex = index;
+ } else {
+ Object.assign(line, {
+ oldLine: bottom ? oldLineNumber + index + 1 : oldLineNumber + index - lineCount,
+ newLine: bottom ? newLineNumber + index + 1 : newLineNumber + index - lineCount,
+ });
+ }
+
+ return line;
+ });
+
+ if (matchLineIndex > -1) {
+ const line = linesWithNumbers[matchLineIndex];
+ const targetLine = bottom
+ ? linesWithNumbers[matchLineIndex - 1]
+ : linesWithNumbers[matchLineIndex + 1];
+
+ Object.assign(line, {
+ metaData: {
+ oldPos: targetLine.oldLine,
+ newPos: targetLine.newLine,
+ },
+ });
+ }
+
+ return linesWithNumbers;
+}
+
+export function addContextLines(options) {
+ const { inlineLines, parallelLines, contextLines, lineNumbers } = options;
+ const normalizedParallelLines = contextLines.map(line => ({
+ left: line,
+ right: line,
+ }));
+
+ if (options.bottom) {
+ inlineLines.push(...contextLines);
+ parallelLines.push(...normalizedParallelLines);
+ } else {
+ const inlineIndex = findIndexInInlineLines(inlineLines, lineNumbers);
+ const parallelIndex = findIndexInParallelLines(parallelLines, lineNumbers);
+ inlineLines.splice(inlineIndex, 0, ...contextLines);
+ parallelLines.splice(parallelIndex, 0, ...normalizedParallelLines);
+ }
+}
+
+export function trimFirstCharOfLineContent(line) {
+ if (!line.richText) {
+ return line;
+ }
+
+ const firstChar = line.richText.charAt(0);
+
+ if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
+ Object.assign(line, {
+ richText: line.richText.substring(1),
+ });
+ }
+
+ return line;
+}
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 866e91057ec..5ecdccf63ad 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -292,7 +292,7 @@
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable) {
- const deployable = this.model.last_deployment.deployable;
+ const { deployable } = this.model.last_deployment;
return `${deployable.name} #${deployable.id}`;
}
return '';
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 5f2989ab854..5ce9225a4bb 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -146,7 +146,7 @@ export default class EnvironmentsStore {
* @return {Array}
*/
updateEnvironmentProp(environment, prop, newValue) {
- const environments = this.state.environments;
+ const { environments } = this.state;
const updatedEnvironments = environments.map((env) => {
const updateEnv = Object.assign({}, env);
@@ -161,7 +161,7 @@ export default class EnvironmentsStore {
}
getOpenFolders() {
- const environments = this.state.environments;
+ const { environments } = this.state;
return environments.filter(env => env.isFolder && env.isOpen);
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 9bc36c1f9b6..27fff488603 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -35,7 +35,7 @@ export default class DropdownUtils {
// Remove the symbol for filter
if (value[0] === filterSymbol) {
- symbol = value[0];
+ [symbol] = value;
value = value.slice(1);
}
@@ -162,7 +162,7 @@ export default class DropdownUtils {
// Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) {
- const container = FilteredSearchContainer.container;
+ const { container } = FilteredSearchContainer;
const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
const values = [];
@@ -220,7 +220,7 @@ export default class DropdownUtils {
}
static getInputSelectionPosition(input) {
- const selectionStart = input.selectionStart;
+ const { selectionStart } = input;
let inputValue = input.value;
// Replace all spaces inside quote marks with underscores
// (will continue to match entire string until an end quote is found if any)
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index d7e1de18d09..296571606d6 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -159,7 +159,7 @@ export default class FilteredSearchDropdownManager {
load(key, firstLoad = false) {
const mappingKey = this.mapping[key];
const glClass = mappingKey.gl;
- const element = mappingKey.element;
+ const { element } = mappingKey;
let forceShowList = false;
if (!mappingKey.reference) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index cf5ba1e1771..81286c54c4c 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -235,7 +235,7 @@ export default class FilteredSearchManager {
checkForEnter(e) {
if (e.keyCode === 38 || e.keyCode === 40) {
- const selectionStart = this.filteredSearchInput.selectionStart;
+ const { selectionStart } = this.filteredSearchInput;
e.preventDefault();
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
@@ -496,7 +496,7 @@ export default class FilteredSearchManager {
// Replace underscore with hyphen in the sanitizedkey.
// e.g. 'my_reaction' => 'my-reaction'
sanitizedKey = sanitizedKey.replace('_', '-');
- const symbol = match.symbol;
+ const { symbol } = match;
let quotationsToUse = '';
if (sanitizedValue.indexOf(' ') !== -1) {
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 600024c21c3..56fe1ab4e90 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -101,7 +101,7 @@ export default class FilteredSearchVisualTokens {
static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
- const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
+ const { baseEndpoint } = filteredSearchInput.dataset;
const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
`${baseEndpoint}/labels.json`,
filteredSearchInput.dataset.endpointQueryParams,
@@ -215,7 +215,7 @@ export default class FilteredSearchVisualTokens {
static addFilterVisualToken(tokenName, tokenValue, canEdit) {
const { lastVisualToken, isLastVisualTokenValid }
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
+ const { addVisualTokenElement } = FilteredSearchVisualTokens;
if (isLastVisualTokenValid) {
addVisualTokenElement(tokenName, tokenValue, false, canEdit);
diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js
index f9338b82acf..c1efa9c86f4 100644
--- a/app/assets/javascripts/filtered_search/recent_searches_root.js
+++ b/app/assets/javascripts/filtered_search/recent_searches_root.js
@@ -29,7 +29,7 @@ class RecentSearchesRoot {
}
render() {
- const state = this.store.state;
+ const { state } = this.store;
this.vm = new Vue({
el: this.wrapperElement,
components: {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 9de57db48fd..09186a865e4 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -7,6 +7,16 @@ function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
}
+export const defaultAutocompleteConfig = {
+ emojis: true,
+ members: true,
+ issues: true,
+ mergeRequests: true,
+ epics: false,
+ milestones: true,
+ labels: true,
+};
+
class GfmAutoComplete {
constructor(dataSources) {
this.dataSources = dataSources || {};
@@ -14,14 +24,7 @@ class GfmAutoComplete {
this.isLoadingData = {};
}
- setup(input, enableMap = {
- emojis: true,
- members: true,
- issues: true,
- milestones: true,
- mergeRequests: true,
- labels: true,
- }) {
+ setup(input, enableMap = defaultAutocompleteConfig) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
this.enableMap = enableMap;
@@ -77,7 +80,7 @@ class GfmAutoComplete {
let tpl = '/${name} ';
let referencePrefix = null;
if (value.params.length > 0) {
- referencePrefix = value.params[0][0];
+ [[referencePrefix]] = value.params;
if (/^[@%~]/.test(referencePrefix)) {
tpl += '<%- referencePrefix %>';
}
@@ -455,7 +458,7 @@ class GfmAutoComplete {
static isLoading(data) {
let dataToInspect = data;
if (data && data.length > 0) {
- dataToInspect = data[0];
+ [dataToInspect] = data;
}
const loadingState = GfmAutoComplete.defaultLoadingData[0];
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 45889c2d604..8d231e6c405 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -613,7 +613,7 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.renderItem = function(data, group, index) {
- var field, fieldName, html, selected, text, url, value, rowHidden;
+ var field, html, selected, text, url, value, rowHidden;
if (!this.options.renderRow) {
value = this.options.id ? this.options.id(data) : data.id;
@@ -651,7 +651,7 @@ GitLabDropdown = (function() {
html = this.options.renderRow.call(this.options, data, this);
} else {
if (!selected) {
- fieldName = this.options.fieldName;
+ const { fieldName } = this.options;
if (value) {
field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
@@ -705,7 +705,8 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
const occurrences = fuzzaldrinPlus.match(text, term);
- const indexOf = [].indexOf;
+ const { indexOf } = [];
+
return text.split('').map(function(character, i) {
if (indexOf.call(occurrences, i) !== -1) {
return "<b>" + character + "</b>";
@@ -721,9 +722,9 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.rowClicked = function(el) {
- var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
+ var field, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
- fieldName = this.options.fieldName;
+ const { fieldName } = this.options;
isInput = $(this.el).is('input');
if (this.renderedData) {
groupName = el.data('group');
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 9f5eba353d7..f802971a3ca 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,14 +1,14 @@
import $ from 'jquery';
import autosize from 'autosize';
-import GfmAutoComplete from './gfm_auto_complete';
+import GfmAutoComplete, * as GFMConfig from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
export default class GLForm {
- constructor(form, enableGFM = false) {
+ constructor(form, enableGFM = {}) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
- this.enableGFM = enableGFM;
+ this.enableGFM = Object.assign({}, GFMConfig.defaultAutocompleteConfig, enableGFM);
// Before we start, we should clean up any previous data for this form
this.destroy();
// Setup the form
@@ -34,14 +34,7 @@ export default class GLForm {
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
- this.autoComplete.setup(this.form.find('.js-gfm-input'), {
- emojis: true,
- members: this.enableGFM,
- issues: this.enableGFM,
- milestones: this.enableGFM,
- mergeRequests: this.enableGFM,
- labels: this.enableGFM,
- });
+ this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
dropzoneInput(this.form);
autosize(this.textarea);
}
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index 57eaac72906..83a9008a94b 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -29,7 +29,7 @@ export default () => {
groupsApp,
},
data() {
- const dataset = this.$options.el.dataset;
+ const { dataset } = this.$options.el;
const hideProjects = dataset.hideProjects === 'true';
const store = new GroupsStore(hideProjects);
const service = new GroupsService(dataset.endpoint);
@@ -42,7 +42,7 @@ export default () => {
};
},
beforeMount() {
- const dataset = this.$options.el.dataset;
+ const { dataset } = this.$options.el;
let groupFilterList = null;
const form = document.querySelector(dataset.formSel);
const filter = document.querySelector(dataset.filterSel);
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 955d9280728..14c74687ab4 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -117,7 +117,7 @@ export default {
class="btn btn-primary btn-sm btn-block"
@click="toggleIsSmall"
>
- {{ __('Commit') }}
+ {{ __('Commit…') }}
</button>
<p
class="text-center"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 5cda7967130..ee21eeda3cd 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -89,14 +89,14 @@ export default {
<template>
<div class="multi-file-commit-list-item position-relative">
- <button
+ <div
v-tooltip
:title="tooltipTitle"
:class="{
'is-active': isActive
}"
- type="button"
class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0"
+ role="button"
@dblclick="fileAction"
@click="openFileInEditor"
>
@@ -107,7 +107,7 @@ export default {
:css-classes="iconClass"
/>{{ file.name }}
</span>
- </button>
+ </div>
<component
:is="actionComponent"
:path="file.path"
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue
index a4cf3edb981..f5252ce7706 100644
--- a/app/assets/javascripts/ide/components/file_finder/item.vue
+++ b/app/assets/javascripts/ide/components/file_finder/item.vue
@@ -30,7 +30,7 @@ export default {
},
computed: {
pathWithEllipsis() {
- const path = this.file.path;
+ const { path } = this.file;
return path.length < MAX_PATH_LENGTH
? path
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 1814924be39..677b282bd61 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -23,6 +23,7 @@
let { result } = target;
if (!isText) {
+ // eslint-disable-next-line prefer-destructuring
result = result.split('base64,')[1];
}
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index dedc2988618..5cd2c9ce188 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -69,7 +69,7 @@ export default {
>
<icon
:size="16"
- name="pipeline"
+ name="rocket"
/>
</button>
</li>
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index c34547fcc60..f490a3a2a39 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -95,24 +95,53 @@ export default {
return this.file.changed || this.file.tempFile || this.file.staged;
},
},
+ mounted() {
+ if (this.hasPathAtCurrentRoute()) {
+ this.scrollIntoView(true);
+ }
+ },
updated() {
if (this.file.type === 'blob' && this.file.active) {
- this.$el.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest',
- });
+ this.scrollIntoView();
}
},
methods: {
...mapActions(['toggleTreeOpen']),
clickFile() {
// Manual Action if a tree is selected/opened
- if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
+ if (this.isTree && this.hasUrlAtCurrentRoute()) {
this.toggleTreeOpen(this.file.path);
}
router.push(`/project${this.file.url}`);
},
+ scrollIntoView(isInit = false) {
+ const block = isInit && this.isTree ? 'center' : 'nearest';
+
+ this.$el.scrollIntoView({
+ behavior: 'smooth',
+ block,
+ });
+ },
+ hasPathAtCurrentRoute() {
+ if (!this.$router || !this.$router.currentRoute) {
+ return false;
+ }
+
+ // - strip route up to "/-/" and ending "/"
+ const routePath = this.$router.currentRoute.path
+ .replace(/^.*?[/]-[/]/g, '')
+ .replace(/[/]$/g, '');
+
+ // - strip ending "/"
+ const filePath = this.file.path
+ .replace(/[/]$/g, '');
+
+ return filePath === routePath;
+ },
+ hasUrlAtCurrentRoute() {
+ return this.$router.currentRoute.path === `/project${this.file.url}`;
+ },
},
};
</script>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 1ad52c1bd83..03772ae4a4c 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -44,6 +44,8 @@ export default {
methods: {
...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
clickFile(tab) {
+ if (tab.active) return;
+
this.updateDelayViewerUpdated(true);
if (tab.pending) {
diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js
index f09930e8158..78b2eab6399 100644
--- a/app/assets/javascripts/ide/lib/diff/diff_worker.js
+++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js
@@ -2,7 +2,7 @@ import { computeDiff } from './diff';
// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', (e) => {
- const data = e.data;
+ const { data } = e;
// eslint-disable-next-line no-restricted-globals
self.postMessage({
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 74f9c112f5a..29995a29d1a 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -8,7 +8,7 @@ import { setPageTitle } from '../utils';
import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
- const path = file.path;
+ const { path } = file;
const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
const fileWasActive = file.active;
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index cc5116413f7..2fbc9990fa2 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -9,6 +9,17 @@ export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_TREE_OPEN, path);
};
+export const showTreeEntry = ({ commit, dispatch, state }, path) => {
+ const entry = state.entries[path];
+ const parentPath = entry ? entry.parentPath : '';
+
+ if (parentPath) {
+ commit(types.SET_TREE_OPEN, parentPath);
+
+ dispatch('showTreeEntry', parentPath);
+ }
+};
+
export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') {
dispatch('toggleTreeOpen', row.path);
@@ -21,6 +32,8 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
} else {
dispatch('getFileData', { path: row.path });
}
+
+ dispatch('showTreeEntry', row.path);
};
export const getLastCommitData = ({ state, commit, dispatch }, tree = state) => {
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 99b315ac4db..fda606dbf01 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -28,6 +28,7 @@ export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
+export const SET_TREE_OPEN = 'SET_TREE_OPEN';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
export const CREATE_TREE = 'CREATE_TREE';
export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES';
diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index 1176c040fb9..2cf34af9274 100644
--- a/app/assets/javascripts/ide/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -6,6 +6,11 @@ export default {
opened: !state.entries[path].opened,
});
},
+ [types.SET_TREE_OPEN](state, path) {
+ Object.assign(state.entries[path], {
+ opened: true,
+ });
+ },
[types.CREATE_TREE](state, { treePath }) {
Object.assign(state, {
trees: Object.assign({}, state.trees, {
diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js
index 12d56714b34..a319bcccb8f 100644
--- a/app/assets/javascripts/image_diff/helpers/dom_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js
@@ -2,7 +2,8 @@ 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 { position } = el.dataset;
+
const positionObject = Object.assign({}, JSON.parse(position), {
x,
y,
diff --git a/app/assets/javascripts/image_diff/helpers/utils_helper.js b/app/assets/javascripts/image_diff/helpers/utils_helper.js
index 28d9a969143..beec99e6934 100644
--- a/app/assets/javascripts/image_diff/helpers/utils_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/utils_helper.js
@@ -40,8 +40,7 @@ export function getTargetSelection(event) {
const x = event.offsetX;
const y = event.offsetY;
- const width = imageEl.width;
- const height = imageEl.height;
+ const { width, height } = imageEl;
const actualWidth = imageEl.naturalWidth;
const actualHeight = imageEl.naturalHeight;
diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js
index 882aedfcc76..3c71258e53b 100644
--- a/app/assets/javascripts/init_notes.js
+++ b/app/assets/javascripts/init_notes.js
@@ -7,10 +7,10 @@ export default () => {
notesIds,
now,
diffView,
- autocomplete,
+ enableGFM,
} = JSON.parse(dataEl.innerHTML);
// Create a singleton so that we don't need to assign
// into the window object, we can just access the current isntance with Notes.instance
- Notes.initialize(notesUrl, notesIds, now, diffView, autocomplete);
+ Notes.initialize(notesUrl, notesIds, now, diffView, enableGFM);
};
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
index b2c2de9e5de..07cf1eff279 100644
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
@@ -10,7 +10,7 @@ class AutoWidthDropdownSelect {
}
init() {
- const dropdownClass = this.dropdownClass;
+ const { dropdownClass } = this;
this.$selectElement.select2({
dropdownCssClass: dropdownClass,
...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index f2939ad4dbe..0db7b95636c 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -4,7 +4,7 @@ import jobHeader from './components/header.vue';
import detailsBlock from './components/sidebar_details_block.vue';
export default () => {
- const dataset = document.getElementById('js-job-details-vue').dataset;
+ const { dataset } = document.getElementById('js-job-details-vue');
const mediator = new JobMediator({ endpoint: dataset.endpoint });
mediator.fetchJob();
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index dfc3f7a94c8..37a45d1d1a2 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -56,7 +56,7 @@ export default class LabelsSelect {
.map(function () {
return this.value;
}).get();
- const handleClick = options.handleClick;
+ const { handleClick } = options;
$sidebarLabelTooltip.tooltip();
@@ -215,7 +215,7 @@ export default class LabelsSelect {
}
else {
if (label.color != null) {
- color = label.color[0];
+ [color] = label.color;
}
}
if (color) {
@@ -243,7 +243,8 @@ export default class LabelsSelect {
var $dropdownParent = $dropdown.parent();
var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
var isSelected = el !== null ? el.hasClass('is-active') : false;
- var title = selected.title;
+
+ var { title } = selected;
var selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
@@ -382,7 +383,7 @@ export default class LabelsSelect {
}));
}
else {
- var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
+ var { labels } = gl.issueBoards.BoardsStore.detail.issue;
labels = labels.filter(function (selectedLabel) {
return selectedLabel.id !== label.id;
});
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index d0b0e5e1ba1..5c249f3068e 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,10 +1,14 @@
import $ from 'jquery';
-import Cookies from 'js-cookie';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
+import { isObject } from './type_utility';
-export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
+export const getPagePath = (index = 0) => {
+ const page = $('body').attr('data-page') || '';
+
+ return page.split(':')[index];
+};
export const isInGroupsPage = () => getPagePath() === 'groups';
@@ -34,17 +38,18 @@ export const checkPageAndAction = (page, action) => {
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
-export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
-export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
-
-export const ajaxGet = url => axios.get(url, {
- params: { format: 'js' },
- responseType: 'text',
-}).then(({ data }) => {
- $.globalEval(data);
-});
-export const rstrip = (val) => {
+export const ajaxGet = url =>
+ axios
+ .get(url, {
+ params: { format: 'js' },
+ responseType: 'text',
+ })
+ .then(({ data }) => {
+ $.globalEval(data);
+ });
+
+export const rstrip = val => {
if (val) {
return val.replace(/\s+$/, '');
}
@@ -60,7 +65,7 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa
closestSubmit.disable();
}
// eslint-disable-next-line func-names
- return field.on(eventName, function () {
+ return field.on(eventName, function() {
if (rstrip($(this).val()) === '') {
return closestSubmit.disable();
}
@@ -79,7 +84,7 @@ export const handleLocationHash = () => {
const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`);
const fixedTabs = document.querySelector('.js-tabs-affix');
- const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck');
+ const fixedDiffStats = document.querySelector('.js-diff-files-changed');
const fixedNav = document.querySelector('.navbar-gitlab');
let adjustment = 0;
@@ -102,7 +107,7 @@ export const handleLocationHash = () => {
// Check if element scrolled into viewport from above or below
// Courtesy http://stackoverflow.com/a/7557433/414749
-export const isInViewport = (el) => {
+export const isInViewport = el => {
const rect = el.getBoundingClientRect();
return (
@@ -113,13 +118,13 @@ export const isInViewport = (el) => {
);
};
-export const parseUrl = (url) => {
+export const parseUrl = url => {
const parser = document.createElement('a');
parser.href = url;
return parser;
};
-export const parseUrlPathname = (url) => {
+export const parseUrlPathname = url => {
const parsedUrl = parseUrl(url);
// parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
// We have to make sure we always have an absolute path.
@@ -128,10 +133,14 @@ export const parseUrlPathname = (url) => {
// We can trust that each param has one & since values containing & will be encoded
// Remove the first character of search as it is always ?
-export const getUrlParamsArray = () => window.location.search.slice(1).split('&').map((param) => {
- const split = param.split('=');
- return [decodeURI(split[0]), split[1]].join('=');
-});
+export const getUrlParamsArray = () =>
+ window.location.search
+ .slice(1)
+ .split('&')
+ .map(param => {
+ const split = param.split('=');
+ return [decodeURI(split[0]), split[1]].join('=');
+ });
export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
@@ -141,18 +150,28 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
-export const scrollToElement = (element) => {
+export const contentTop = () => {
+ const perfBar = $('#js-peek').height() || 0;
+ const mrTabsHeight = $('.merge-request-tabs').height() || 0;
+ const headerHeight = $('.navbar-gitlab').height() || 0;
+ const diffFilesChanged = $('.js-diff-files-changed').height() || 0;
+
+ return perfBar + mrTabsHeight + headerHeight + diffFilesChanged;
+};
+
+export const scrollToElement = element => {
let $el = element;
if (!(element instanceof $)) {
$el = $(element);
}
- const top = $el.offset().top;
- const mrTabsHeight = $('.merge-request-tabs').height() || 0;
- const headerHeight = $('.navbar-gitlab').height() || 0;
+ const { top } = $el.offset();
- return $('body, html').animate({
- scrollTop: top - mrTabsHeight - headerHeight,
- }, 200);
+ return $('body, html').animate(
+ {
+ scrollTop: top - contentTop(),
+ },
+ 200,
+ );
};
/**
@@ -184,9 +203,7 @@ export const getSelectedFragment = () => {
export const insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
- const selectionStart = target.selectionStart;
- const selectionEnd = target.selectionEnd;
- const value = target.value;
+ const { selectionStart, selectionEnd, value } = target;
const textBefore = value.substring(0, selectionStart);
const textAfter = value.substring(selectionEnd, value.length);
@@ -212,7 +229,8 @@ export const insertText = (target, text) => {
};
export const nodeMatchesSelector = (node, selector) => {
- const matches = Element.prototype.matches ||
+ const matches =
+ Element.prototype.matches ||
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
@@ -225,7 +243,8 @@ export const nodeMatchesSelector = (node, selector) => {
// IE11 doesn't support `node.matches(selector)`
- let parentNode = node.parentNode;
+ let { parentNode } = node;
+
if (!parentNode) {
parentNode = document.createElement('div');
// eslint-disable-next-line no-param-reassign
@@ -241,10 +260,10 @@ export const nodeMatchesSelector = (node, selector) => {
this will take in the headers from an API response and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
-export const normalizeHeaders = (headers) => {
+export const normalizeHeaders = headers => {
const upperCaseHeaders = {};
- Object.keys(headers || {}).forEach((e) => {
+ Object.keys(headers || {}).forEach(e => {
upperCaseHeaders[e.toUpperCase()] = headers[e];
});
@@ -255,12 +274,14 @@ export const normalizeHeaders = (headers) => {
this will take in the getAllResponseHeaders result and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
-export const normalizeCRLFHeaders = (headers) => {
+export const normalizeCRLFHeaders = headers => {
const headersObject = {};
const headersArray = headers.split('\n');
- headersArray.forEach((header) => {
+ headersArray.forEach(header => {
const keyValue = header.split(': ');
+
+ // eslint-disable-next-line prefer-destructuring
headersObject[keyValue[0]] = keyValue[1];
});
@@ -295,15 +316,13 @@ export const parseIntPagination = paginationInformation => ({
export const parseQueryStringIntoObject = (query = '') => {
if (query === '') return {};
- return query
- .split('&')
- .reduce((acc, element) => {
- const val = element.split('=');
- Object.assign(acc, {
- [val[0]]: decodeURIComponent(val[1]),
- });
- return acc;
- }, {});
+ return query.split('&').reduce((acc, element) => {
+ const val = element.split('=');
+ Object.assign(acc, {
+ [val[0]]: decodeURIComponent(val[1]),
+ });
+ return acc;
+ }, {});
};
/**
@@ -312,9 +331,13 @@ export const parseQueryStringIntoObject = (query = '') => {
*
* @param {Object} params
*/
-export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&');
+export const objectToQueryString = (params = {}) =>
+ Object.keys(params)
+ .map(param => `${param}=${params[param]}`)
+ .join('&');
-export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
+export const buildUrlWithCurrentLocation = param =>
+ (param ? `${window.location.pathname}${param}` : window.location.pathname);
/**
* Based on the current location and the string parameters provided
@@ -322,7 +345,7 @@ export const buildUrlWithCurrentLocation = param => (param ? `${window.location.
*
* @param {String} param
*/
-export const historyPushState = (newUrl) => {
+export const historyPushState = newUrl => {
window.history.pushState({}, document.title, newUrl);
};
@@ -371,7 +394,7 @@ export const backOff = (fn, timeout = 60000) => {
let timeElapsed = 0;
return new Promise((resolve, reject) => {
- const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
+ const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => {
if (timeElapsed < timeout) {
@@ -447,7 +470,8 @@ export const resetFavicon = () => {
};
export const setCiStatusFavicon = pageUrl =>
- axios.get(pageUrl)
+ axios
+ .get(pageUrl)
.then(({ data }) => {
if (data && data.favicon) {
return setFaviconOverlay(data.favicon);
@@ -469,28 +493,38 @@ export const spriteIcon = (icon, className = '') => {
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
*/
-export const convertObjectPropsToCamelCase = (obj = {}) => {
+export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
if (obj === null) {
return {};
}
+ const initial = Array.isArray(obj) ? [] : {};
+
return Object.keys(obj).reduce((acc, prop) => {
const result = acc;
+ const val = obj[prop];
- result[convertToCamelCase(prop)] = obj[prop];
+ if (options.deep && (isObject(val) || Array.isArray(val))) {
+ result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options);
+ } else {
+ result[convertToCamelCase(prop)] = obj[prop];
+ }
return acc;
- }, {});
+ }, initial);
};
-export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
+export const imagePath = imgUrl =>
+ `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
$(selector).on('focusin', function selectOnFocusCallback() {
- $(this).select().one('mouseup', (e) => {
- e.preventDefault();
- });
+ $(this)
+ .select()
+ .one('mouseup', e => {
+ e.preventDefault();
+ });
});
};
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index 914de9de940..6f42382246d 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -1,7 +1,4 @@
-import $ from 'jquery';
-import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie } from './common_utils';
-
-const isVueMRDiscussions = () => isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
+import { isInIssuePage, isInMRPage, isInEpicPage } from './common_utils';
export const addClassIfElementExists = (element, className) => {
if (element) {
@@ -9,4 +6,4 @@ export const addClassIfElementExists = (element, className) => {
}
};
-export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isVueMRDiscussions();
+export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isInMRPage();
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index f086d962221..afbab59055b 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -13,7 +13,7 @@ export function formatRelevantDigits(number) {
let relevantDigits = 0;
let formattedNumber = '';
if (!Number.isNaN(Number(number))) {
- digitsLeft = number.toString().split('.')[0];
+ [digitsLeft] = number.toString().split('.');
switch (digitsLeft.length) {
case 1:
relevantDigits = 3;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 5e786ee6935..5f25c6ce1ae 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -58,6 +58,14 @@ export const slugify = str => str.trim().toLowerCase();
export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3)}...`;
/**
+ * Truncate SHA to 8 characters
+ *
+ * @param {String} sha
+ * @returns {String}
+ */
+export const truncateSha = sha => sha.substr(0, 8);
+
+/**
* Capitalizes first character
*
* @param {String} text
@@ -98,3 +106,16 @@ export const convertToSentenceCase = string => {
return splitWord.join(' ');
};
+
+/**
+ * Splits camelCase or PascalCase words
+ * e.g. HelloWorld => Hello World
+ *
+ * @param {*} string
+*/
+export const splitCamelCase = string => (
+ string
+ .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2')
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
+ .trim()
+);
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 815b13f7bf5..291655235d5 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -142,14 +142,14 @@ LineHighlighter.prototype.highlightLine = function(lineNumber) {
//
// range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
- var i, lineNumber, ref, ref1, results;
if (range[1]) {
- results = [];
+ const results = [];
+ const ref = range[0] <= range[1] ? range : range.reverse();
- // eslint-disable-next-line no-multi-assign
- for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
+ for (let lineNumber = range[0]; lineNumber <= ref[1]; lineNumber += 1) {
results.push(this.highlightLine(lineNumber));
}
+
return results;
} else {
return this.highlightLine(range[0]);
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index 70f185e3656..1501296ac4f 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -156,7 +156,7 @@ import Cookies from 'js-cookie';
return 0;
}
- const files = this.state.conflictsData.files;
+ const { files } = this.state.conflictsData;
let count = 0;
files.forEach((file) => {
@@ -313,7 +313,7 @@ import Cookies from 'js-cookie';
},
isReadyToCommit() {
- const files = this.state.conflictsData.files;
+ const { files } = this.state.conflictsData;
const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length;
let unresolved = 0;
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 491858c3602..7badd68089c 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -12,7 +12,7 @@ import syntaxHighlight from '../syntax_highlight';
export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const conflictsEl = document.querySelector('#conflicts');
- const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
+ const { mergeConflictsStore } = gl.mergeConflicts;
const mergeConflictsService = new MergeConflictsService({
conflictsPath: conflictsEl.dataset.conflictsPath,
resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath,
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 83decc1d298..7bf2c56dd5d 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -49,6 +49,7 @@ MergeRequest.prototype.initTabs = function() {
if (window.mrTabs) {
window.mrTabs.unbindEvents();
}
+
window.mrTabs = new MergeRequestTabs(this.opts);
};
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 65ab41559be..329d4303132 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,6 +1,7 @@
/* eslint-disable no-new, class-methods-use-this */
import $ from 'jquery';
+import Vue from 'vue';
import Cookies from 'js-cookie';
import axios from './lib/utils/axios_utils';
import flash from './flash';
@@ -8,6 +9,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
+import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { getLocationHash } from './lib/utils/url_utility';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
@@ -62,7 +64,7 @@ import Notes from './notes';
/* eslint-enable max-len */
// Store the `location` object, allowing for easier stubbing in tests
-let location = window.location;
+let { location } = window;
export default class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
@@ -70,11 +72,13 @@ export default class MergeRequestTabs {
const navbar = document.querySelector('.navbar-gitlab');
const peek = document.getElementById('js-peek');
const paddingTop = 16;
+ this.commitsTab = document.querySelector('.tab-content .commits.tab-pane');
this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
+ this.eventHub = new Vue();
this.setUrl = setUrl !== undefined ? setUrl : true;
this.setCurrentAction = this.setCurrentAction.bind(this);
@@ -149,7 +153,9 @@ export default class MergeRequestTabs {
this.resetViewContainer();
this.destroyPipelinesView();
} else if (this.isDiffAction(action)) {
- this.loadDiff($target.attr('href'));
+ if (!isInVueNoteablePage()) {
+ this.loadDiff($target.attr('href'));
+ }
if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
@@ -157,6 +163,7 @@ export default class MergeRequestTabs {
this.expandViewContainer();
}
this.destroyPipelinesView();
+ this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') {
this.resetViewContainer();
this.mountPipelinesView();
@@ -172,6 +179,8 @@ export default class MergeRequestTabs {
if (this.setUrl) {
this.setCurrentAction(action);
}
+
+ this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
}
scrollToElement(container) {
@@ -270,7 +279,7 @@ export default class MergeRequestTabs {
mountPipelinesView() {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- const CommitPipelinesTable = gl.CommitPipelinesTable;
+ const { CommitPipelinesTable } = gl;
this.commitPipelinesTable = new CommitPipelinesTable({
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index ed3a27dd68b..cee39fd0559 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -41,10 +41,10 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
} else {
const unusedColors = _.difference(defaultColorOrder, usedColors);
if (unusedColors.length > 0) {
- pick = unusedColors[0];
+ [pick] = unusedColors;
} else {
usedColors = [];
- pick = defaultColorOrder[0];
+ [pick] = defaultColorOrder;
}
}
usedColors.push(pick);
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index e3c5bf06b3d..3c0c9995cc2 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -1,20 +1,32 @@
+import $ from 'jquery';
import Vue from 'vue';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import initDiffsApp from '../diffs';
import notesApp from '../notes/components/notes_app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue';
-import store from '../notes/stores';
+import store from './stores';
+import MergeRequest from '../merge_request';
export default function initMrNotes() {
+ const mrShowNode = document.querySelector('.merge-request');
+ // eslint-disable-next-line no-new
+ new MergeRequest({
+ action: mrShowNode.dataset.mrAction,
+ });
+
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-mr-discussions',
+ name: 'MergeRequestDiscussions',
components: {
notesApp,
},
+ store,
data() {
- const notesDataset = document.getElementById('js-vue-mr-discussions')
- .dataset;
+ const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
const noteableData = JSON.parse(notesDataset.noteableData);
noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
return {
noteableData,
@@ -22,12 +34,42 @@ export default function initMrNotes() {
notesData: JSON.parse(notesDataset.notesData),
};
},
+ computed: {
+ ...mapGetters(['discussionTabCounter']),
+ ...mapState({
+ activeTab: state => state.page.activeTab,
+ }),
+ },
+ watch: {
+ discussionTabCounter() {
+ this.updateDiscussionTabCounter();
+ },
+ },
+ mounted() {
+ this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge');
+ this.setActiveTab(window.mrTabs.getCurrentAction());
+
+ window.mrTabs.eventHub.$on('MergeRequestTabChange', tab => {
+ this.setActiveTab(tab);
+ });
+ $(document).on('visibilitychange', this.updateDiscussionTabCounter);
+ },
+ beforeDestroy() {
+ $(document).off('visibilitychange', this.updateDiscussionTabCounter);
+ },
+ methods: {
+ ...mapActions(['setActiveTab']),
+ updateDiscussionTabCounter() {
+ this.notesCountBadge.text(this.discussionTabCounter);
+ },
+ },
render(createElement) {
return createElement('notes-app', {
props: {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
+ shouldShow: this.activeTab === 'show',
},
});
},
@@ -36,6 +78,7 @@ export default function initMrNotes() {
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-discussion-counter',
+ name: 'DiscussionCounter',
components: {
discussionCounter,
},
@@ -44,4 +87,6 @@ export default function initMrNotes() {
return createElement('discussion-counter');
},
});
+
+ initDiffsApp(store);
}
diff --git a/app/assets/javascripts/mr_notes/stores/actions.js b/app/assets/javascripts/mr_notes/stores/actions.js
new file mode 100644
index 00000000000..426c6a00d5e
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/actions.js
@@ -0,0 +1,7 @@
+import types from './mutation_types';
+
+export default {
+ setActiveTab({ commit }, tab) {
+ commit(types.SET_ACTIVE_TAB, tab);
+ },
+};
diff --git a/app/assets/javascripts/mr_notes/stores/getters.js b/app/assets/javascripts/mr_notes/stores/getters.js
new file mode 100644
index 00000000000..b10e9f9f9f1
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/getters.js
@@ -0,0 +1,5 @@
+export default {
+ isLoggedIn(state, getters) {
+ return !!getters.getUserData.id;
+ },
+};
diff --git a/app/assets/javascripts/mr_notes/stores/index.js b/app/assets/javascripts/mr_notes/stores/index.js
new file mode 100644
index 00000000000..dd2019001db
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import notesModule from '~/notes/stores/modules';
+import diffsModule from '~/diffs/store/modules';
+import mrPageModule from './modules';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ modules: {
+ page: mrPageModule,
+ notes: notesModule,
+ diffs: diffsModule,
+ },
+});
diff --git a/app/assets/javascripts/mr_notes/stores/modules/index.js b/app/assets/javascripts/mr_notes/stores/modules/index.js
new file mode 100644
index 00000000000..660081f76c8
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/modules/index.js
@@ -0,0 +1,12 @@
+import actions from '../actions';
+import getters from '../getters';
+import mutations from '../mutations';
+
+export default {
+ state: {
+ activeTab: null,
+ },
+ actions,
+ getters,
+ mutations,
+};
diff --git a/app/assets/javascripts/mr_notes/stores/mutation_types.js b/app/assets/javascripts/mr_notes/stores/mutation_types.js
new file mode 100644
index 00000000000..105104361cf
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/mutation_types.js
@@ -0,0 +1,3 @@
+export default {
+ SET_ACTIVE_TAB: 'SET_ACTIVE_TAB',
+};
diff --git a/app/assets/javascripts/mr_notes/stores/mutations.js b/app/assets/javascripts/mr_notes/stores/mutations.js
new file mode 100644
index 00000000000..8175aa9488f
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/mutations.js
@@ -0,0 +1,7 @@
+import types from './mutation_types';
+
+export default {
+ [types.SET_ACTIVE_TAB](state, tab) {
+ Object.assign(state, { activeTab: tab });
+ },
+};
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 7787849f7a8..94da1be4066 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -101,8 +101,8 @@ export default (function() {
};
BranchGraph.prototype.buildGraph = function() {
- var cuday, cumonth, day, j, len, mm, r, ref;
- r = this.r;
+ var cuday, cumonth, day, j, len, mm, ref;
+ const { r } = this;
cuday = 0;
cumonth = "";
r.rect(0, 0, 40, this.barHeight).attr({
@@ -113,8 +113,7 @@ export default (function() {
});
ref = this.days;
- // eslint-disable-next-line no-multi-assign
- for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
+ for (mm = 0, len = ref.length; mm < len; mm += 1) {
day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
// Dates
@@ -122,7 +121,7 @@ export default (function() {
font: "12px Monaco, monospace",
fill: "#BBB"
});
- cuday = day[0];
+ [cuday] = day;
}
if (cumonth !== day[1]) {
// Months
@@ -130,6 +129,8 @@ export default (function() {
font: "12px Monaco, monospace",
fill: "#EEE"
});
+
+ // eslint-disable-next-line prefer-destructuring
cumonth = day[1];
}
}
@@ -170,8 +171,8 @@ export default (function() {
};
BranchGraph.prototype.bindEvents = function() {
- var element;
- element = this.element;
+ const { element } = this;
+
return $(element).scroll((function(_this) {
return function(event) {
return _this.renderPartialGraph();
@@ -208,11 +209,13 @@ export default (function() {
};
BranchGraph.prototype.appendLabel = function(x, y, commit) {
- var label, r, rect, shortrefs, text, textbox, triangle;
+ var label, rect, shortrefs, text, textbox, triangle;
+
if (!commit.refs) {
return;
}
- r = this.r;
+
+ const { r } = this;
shortrefs = commit.refs;
// Truncate if longer than 15 chars
if (shortrefs.length > 17) {
@@ -243,11 +246,8 @@ export default (function() {
};
BranchGraph.prototype.appendAnchor = function(x, y, commit) {
- var anchor, options, r, top;
- r = this.r;
- top = this.top;
- options = this.options;
- anchor = r.circle(x, y, 10).attr({
+ const { r, top, options } = this;
+ const anchor = r.circle(x, y, 10).attr({
fill: "#000",
opacity: 0,
cursor: "pointer"
@@ -263,14 +263,15 @@ export default (function() {
};
BranchGraph.prototype.drawDot = function(x, y, commit) {
- var avatar_box_x, avatar_box_y, r;
- r = this.r;
+ const { r } = this;
r.circle(x, y, 3).attr({
fill: this.colors[commit.space],
stroke: "none"
});
- avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10;
- avatar_box_y = y - 10;
+
+ const avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10;
+ const avatar_box_y = y - 10;
+
r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({
stroke: this.colors[commit.space],
"stroke-width": 2
@@ -283,13 +284,12 @@ export default (function() {
};
BranchGraph.prototype.drawLines = function(x, y, commit) {
- var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route;
- r = this.r;
- ref = commit.parents;
- results = [];
+ var arrow, color, i, len, offset, parent, parentCommit, parentX1, parentX2, parentY, route;
+ const { r } = this;
+ const ref = commit.parents;
+ const results = [];
- // eslint-disable-next-line no-multi-assign
- for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
+ for (i = 0, len = ref.length; i < len; i += 1) {
parent = ref[i];
parentCommit = this.preparedCommits[parent[0]];
parentY = this.offsetY + this.unitTime * parentCommit.time;
@@ -333,11 +333,10 @@ export default (function() {
};
BranchGraph.prototype.markCommit = function(commit) {
- var r, x, y;
if (commit.id === this.options.commit_id) {
- r = this.r;
- x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
- y = this.offsetY + this.unitTime * commit.time;
+ const { r } = this;
+ const x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
+ const y = this.offsetY + this.unitTime * commit.time;
r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({
fill: "#000",
"fill-opacity": .5,
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 41ba5b28a1b..205d9766656 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -52,7 +52,7 @@ export default class NewBranchForm {
validate() {
var errorMessage, errors, formatter, unique, validator;
- const indexOf = [].indexOf;
+ const { indexOf } = [];
this.branchNameError.empty();
unique = function(values, value) {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index d23939c00e1..48cda28a1ae 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -20,6 +20,7 @@ import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_c
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
+import { defaultAutocompleteConfig } from './gfm_auto_complete';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
@@ -30,7 +31,7 @@ import {
getPagePath,
scrollToElement,
isMetaKey,
- hasVueMRDiscussionsCookie,
+ isInMRPage,
} from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
@@ -45,21 +46,9 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export default class Notes {
- static initialize(
- notes_url,
- note_ids,
- last_fetched_at,
- view,
- enableGFM = true,
- ) {
+ static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM) {
if (!this.instance) {
- this.instance = new Notes(
- notes_url,
- note_ids,
- last_fetched_at,
- view,
- enableGFM,
- );
+ this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
}
}
@@ -67,7 +56,7 @@ export default class Notes {
return this.instance;
}
- constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
+ constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = defaultAutocompleteConfig) {
this.updateTargetButtons = this.updateTargetButtons.bind(this);
this.updateComment = this.updateComment.bind(this);
this.visibilityChange = this.visibilityChange.bind(this);
@@ -102,13 +91,11 @@ export default class Notes {
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
- this.$wrapperEl = hasVueMRDiscussionsCookie()
- ? $(document).find('.diffs')
- : $(document);
+ this.$wrapperEl = isInMRPage() ? $(document).find('.diffs') : $(document);
this.cleanBinding();
this.addBinding();
this.setPollingInterval();
- this.setupMainTargetNoteForm();
+ this.setupMainTargetNoteForm(enableGFM);
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
@@ -144,55 +131,27 @@ export default class Notes {
// Reopen and close actions for Issue/MR combined with note form submit
this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment);
this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment);
- this.$wrapperEl.on(
- 'keyup input',
- '.js-note-text',
- this.updateTargetButtons,
- );
+ this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons);
// resolve a discussion
this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general)
this.$wrapperEl.on('click', '.js-note-delete', this.removeNote);
// delete note attachment
- this.$wrapperEl.on(
- 'click',
- '.js-note-attachment-delete',
- this.removeAttachment,
- );
+ this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment);
// reset main target form when clicking discard
this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected
- this.$wrapperEl.on(
- 'change',
- '.js-note-attachment-input',
- this.updateFormAttachment,
- );
+ this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
- this.$wrapperEl.on(
- 'click',
- '.js-discussion-reply-button',
- this.onReplyToDiscussionNote,
- );
+ this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
- this.$wrapperEl.on(
- 'click',
- '.js-add-image-diff-note-button',
- this.onAddImageDiffNote,
- );
+ this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
// hide diff note form
- this.$wrapperEl.on(
- 'click',
- '.js-close-discussion-note-form',
- this.cancelDiscussionForm,
- );
+ this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list
- this.$wrapperEl.on(
- 'click',
- '.system-note-commit-list-toggler',
- this.toggleCommitList,
- );
+ this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this));
@@ -203,16 +162,8 @@ export default class Notes {
this.$wrapperEl.on('issuable:change', this.refresh);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote);
- this.$wrapperEl.on(
- 'ajax:success',
- '.js-discussion-note-form',
- this.addDiscussionNote,
- );
- this.$wrapperEl.on(
- 'ajax:success',
- '.js-main-target-form',
- this.resetMainTargetForm,
- );
+ this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
+ this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
this.$wrapperEl.on(
'ajax:complete',
'.js-main-target-form',
@@ -222,8 +173,6 @@ export default class Notes {
this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText);
// When the URL fragment/hash has changed, `#note_xxx`
$(window).on('hashchange', this.onHashChange);
- this.boundGetContent = this.getContent.bind(this);
- document.addEventListener('refreshLegacyNotes', this.boundGetContent);
}
cleanBinding() {
@@ -247,21 +196,14 @@ export default class Notes {
this.$wrapperEl.off('ajax:success', '.js-main-target-form');
this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
- document.removeEventListener('refreshLegacyNotes', this.boundGetContent);
$(window).off('hashchange', this.onHashChange);
}
static initCommentTypeToggle(form) {
- const dropdownTrigger = form.querySelector(
- '.js-comment-type-dropdown .dropdown-toggle',
- );
- const dropdownList = form.querySelector(
- '.js-comment-type-dropdown .dropdown-menu',
- );
+ const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle');
+ const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu');
const noteTypeInput = form.querySelector('#note_type');
- const submitButton = form.querySelector(
- '.js-comment-type-dropdown .js-comment-submit-button',
- );
+ const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button');
const closeButton = form.querySelector('.js-note-target-close');
const reopenButton = form.querySelector('.js-note-target-reopen');
@@ -297,9 +239,7 @@ export default class Notes {
return;
}
myLastNote = $(
- `li.note[data-author-id='${
- gon.current_user_id
- }'][data-editable]:last`,
+ `li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`,
$textarea.closest('.note, .notes_holder, #notes'),
);
if (myLastNote.length) {
@@ -371,7 +311,7 @@ export default class Notes {
},
})
.then(({ data }) => {
- const notes = data.notes;
+ const { notes } = data;
this.last_fetched_at = data.last_fetched_at;
this.setPollingInterval(data.notes.length);
$.each(notes, (i, note) => this.renderNote(note));
@@ -396,8 +336,7 @@ export default class Notes {
if (shouldReset == null) {
shouldReset = true;
}
- nthInterval =
- this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
+ nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
if (shouldReset) {
this.pollingInterval = this.basePollingInterval;
} else if (this.pollingInterval < nthInterval) {
@@ -418,10 +357,7 @@ export default class Notes {
loadAwardsHandler()
.then(awardsHandler => {
- awardsHandler.addAwardToEmojiBar(
- votesBlock,
- noteEntity.commands_changes.emoji_award,
- );
+ awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
awardsHandler.scrollToAwards();
})
.catch(() => {
@@ -471,17 +407,10 @@ export default class Notes {
if (!noteEntity.valid) {
if (noteEntity.errors && noteEntity.errors.commands_only) {
- if (
- noteEntity.commands_changes &&
- Object.keys(noteEntity.commands_changes).length > 0
- ) {
+ if (noteEntity.commands_changes && Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
- this.addFlash(
- noteEntity.errors.commands_only,
- 'notice',
- this.parentTimeline.get(0),
- );
+ this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0));
this.refresh();
}
return;
@@ -489,7 +418,7 @@ export default class Notes {
const $note = $notesList.find(`#note_${noteEntity.id}`);
if (Notes.isNewNote(noteEntity, this.note_ids)) {
- if (hasVueMRDiscussionsCookie()) {
+ if (isInMRPage()) {
return;
}
@@ -517,8 +446,7 @@ export default class Notes {
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
const isTextareaUntouched =
- currentContent === initialContent ||
- currentContent === sanitizedNoteNote;
+ currentContent === initialContent || currentContent === sanitizedNoteNote;
if (isEditing && isTextareaUntouched) {
$textarea.val(noteEntity.note);
@@ -531,8 +459,6 @@ export default class Notes {
this.setupNewNote($updatedNote);
}
}
-
- Notes.refreshVueNotes();
}
isParallelView() {
@@ -550,13 +476,7 @@ export default class Notes {
}
this.note_ids.push(noteEntity.id);
- form =
- $form ||
- $(
- `.js-discussion-note-form[data-discussion-id="${
- noteEntity.discussion_id
- }"]`,
- );
+ form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row =
form.length || !noteEntity.discussion_line_code
? form.closest('tr')
@@ -572,9 +492,7 @@ export default class Notes {
.first()
.find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
- discussionContainer = $(
- `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
- );
+ discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes');
}
@@ -582,18 +500,12 @@ 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') ||
- noteEntity.on_image
- ) {
+ 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 {
// Merge new discussion HTML in
- var $notes = $discussion.find(
- `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
- );
+ var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
var contentContainerClass =
'.' +
$notes
@@ -606,29 +518,15 @@ export default class Notes {
.find(contentContainerClass + ' .content')
.append($notes.closest('.content').children());
}
- }
- // Init discussion on 'Discussion' page if it is merge request page
- const page = $('body').attr('data-page');
- if (
- (page && page.indexOf('projects:merge_request') !== -1) ||
- !noteEntity.diff_discussion_html
- ) {
- if (!hasVueMRDiscussionsCookie()) {
- Notes.animateAppendNote(
- noteEntity.discussion_html,
- $('.main-notes-list'),
- );
- }
+ } else {
+ Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
}
} else {
// append new note to all matching discussions
Notes.animateAppendNote(noteEntity.html, discussionContainer);
}
- if (
- typeof gl.diffNotesCompileComponents !== 'undefined' &&
- noteEntity.discussion_resolvable
- ) {
+ if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
@@ -701,14 +599,14 @@ export default class Notes {
*
* Sets some hidden fields in the form.
*/
- setupMainTargetNoteForm() {
+ setupMainTargetNoteForm(enableGFM) {
var form;
// find the form
form = $('.js-new-note-form');
// Set a global clone of the form for later cloning
this.formClone = form.clone();
// show the form
- this.setupNoteForm(form);
+ this.setupNoteForm(form, enableGFM);
// fix classes
form.removeClass('js-new-note-form');
form.addClass('js-main-target-form');
@@ -736,9 +634,9 @@ export default class Notes {
* setup GFM auto complete
* show the form
*/
- setupNoteForm(form) {
+ setupNoteForm(form, enableGFM = defaultAutocompleteConfig) {
var textarea, key;
- this.glForm = new GLForm(form, this.enableGFM);
+ this.glForm = new GLForm(form, enableGFM);
textarea = form.find('.js-note-text');
key = [
'Note',
@@ -938,9 +836,7 @@ export default class Notes {
form.removeClass('current-note-edit-form');
form.find('.js-finish-edit-warning').hide();
// Replace markdown textarea text with original note text.
- return form
- .find('.js-note-text')
- .val(form.find('form.edit-note').data('originalNote'));
+ return form.find('.js-note-text').val(form.find('form.edit-note').data('originalNote'));
}
/**
@@ -988,21 +884,15 @@ export default class Notes {
// The notes tr can contain multiple lists of notes, like on the parallel diff
// notesTr does not exist for image diffs
- if (
- notesTr.find('.discussion-notes').length > 1 ||
- notesTr.length === 0
- ) {
+ 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,
- },
+ 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);
}
@@ -1016,7 +906,6 @@ export default class Notes {
})(this),
);
- Notes.refreshVueNotes();
Notes.checkMergeRequestStatus();
return this.updateNotesCount(-1);
}
@@ -1032,7 +921,7 @@ export default class Notes {
$note.find('.note-attachment').remove();
$note.find('.note-body > .note-text').show();
$note.find('.note-header').show();
- return $note.find('.current-note-edit-form').remove();
+ return $note.find('.diffs .current-note-edit-form').remove();
}
/**
@@ -1106,9 +995,7 @@ export default class Notes {
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
- form
- .removeClass('js-main-target-form')
- .addClass('discussion-form js-discussion-note-form');
+ form.removeClass('js-main-target-form').addClass('discussion-form js-discussion-note-form');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn');
@@ -1118,9 +1005,7 @@ export default class Notes {
}
form.find('.js-note-text').focus();
- form
- .find('.js-comment-resolve-button')
- .attr('data-discussion-id', discussionID);
+ form.find('.js-comment-resolve-button').attr('data-discussion-id', discussionID);
}
/**
@@ -1153,9 +1038,7 @@ export default class Notes {
// Setup comment form
let newForm;
- const $noteContainer = $link
- .closest('.diff-viewer')
- .find('.note-container');
+ const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
const $form = $noteContainer.find('> .discussion-form');
if ($form.length === 0) {
@@ -1224,9 +1107,7 @@ export default class Notes {
notesContent = targetRow.find(notesContentSelector);
addForm = true;
} else {
- const isCurrentlyShown = targetRow
- .find('.content:not(:empty)')
- .is(':visible');
+ const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
@@ -1391,9 +1272,7 @@ export default class Notes {
if ($note.find('.js-conflict-edit-warning').length === 0) {
const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
This comment has changed since you started editing, please review the
- <a href="#note_${
- noteEntity.id
- }" target="_blank" rel="noopener noreferrer">
+ <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">
updated comment
</a>
to ensure information is not lost
@@ -1403,9 +1282,7 @@ export default class Notes {
}
updateNotesCount(updateCount) {
- return this.notesCountBadge.text(
- parseInt(this.notesCountBadge.text(), 10) + updateCount,
- );
+ return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
}
static renderPlaceholderComponent($container) {
@@ -1482,9 +1359,7 @@ export default class Notes {
toggleCommitList(e) {
const $element = $(e.currentTarget);
- const $closestSystemCommitList = $element.siblings(
- '.system-note-commit-list',
- );
+ const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
$element
.find('.fa')
@@ -1517,9 +1392,7 @@ export default class Notes {
$systemNote.find('.note-text').addClass('system-note-commit-list');
$systemNote.find('.system-note-commit-list-toggler').show();
} else {
- $systemNote
- .find('.note-text')
- .addClass('system-note-commit-list hide-shade');
+ $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade');
}
});
}
@@ -1590,10 +1463,6 @@ export default class Notes {
return $updatedNote;
}
- static refreshVueNotes() {
- document.dispatchEvent(new CustomEvent('refreshVueNotes'));
- }
-
/**
* Get data from Form attributes to use for saving/submitting comment.
*/
@@ -1752,15 +1621,8 @@ export default class Notes {
.attr('id') === 'discussion';
const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
- const isDiscussionResolve = $submitBtn.hasClass(
- 'js-comment-resolve-button',
- );
- const {
- formData,
- formContent,
- formAction,
- formContentOriginal,
- } = this.getFormData($form);
+ const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
+ const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
let noteUniqueId;
let systemNoteUniqueId;
let hasQuickActions = false;
@@ -1847,9 +1709,7 @@ export default class Notes {
// Reset cached commands list when command is applied
if (hasQuickActions) {
- $form
- .find('textarea.js-note-text')
- .trigger('clear-commands-cache.atwho');
+ $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
}
// Clear previous form errors
@@ -1894,12 +1754,8 @@ export default class Notes {
// append flash-container to the Notes list
if ($notesContainer.length) {
- $notesContainer.append(
- '<div class="flash-container" style="display: none;"></div>',
- );
+ $notesContainer.append('<div class="flash-container" style="display: none;"></div>');
}
-
- Notes.refreshVueNotes();
} else if (isMainForm) {
// Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup
@@ -1933,9 +1789,7 @@ export default class Notes {
// Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) {
- const replyButton = $notesContainer
- .parent()
- .find('.js-discussion-reply-button');
+ const replyButton = $notesContainer.parent().find('.js-discussion-reply-button');
this.replyToDiscussionNote(replyButton[0]);
$form = $notesContainer.parent().find('form');
}
@@ -1978,9 +1832,7 @@ export default class Notes {
// Show updated comment content temporarily
$noteBodyText.html(formContent);
- $editingNote
- .removeClass('is-editing fade-in-full')
- .addClass('being-posted fade-in-half');
+ $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
$editingNote
.find('.note-headline-meta a')
.html(
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index ad6dd3d9a09..c6a524f68cb 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -7,10 +7,7 @@ import { __, sprintf } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
-import {
- capitalizeFirstCharacter,
- convertToCamelCase,
-} from '../../lib/utils/text_utility';
+import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase } from '../../lib/utils/text_utility';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
@@ -56,21 +53,23 @@ export default {
]),
...mapState(['isToggleStateButtonLoading']),
noteableDisplayName() {
- return this.noteableType.replace(/_/g, ' ');
+ return splitCamelCase(this.noteableType).toLowerCase();
},
isLoggedIn() {
return this.getUserData.id;
},
commentButtonTitle() {
- return this.noteType === constants.COMMENT
- ? 'Comment'
- : 'Start discussion';
+ return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
+ },
+ startDiscussionDescription() {
+ let text = 'Discuss a specific suggestion or question';
+ if (this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE) {
+ text += ' that needs to be resolved';
+ }
+ return `${text}.`;
},
isOpen() {
- return (
- this.openState === constants.OPENED ||
- this.openState === constants.REOPENED
- );
+ return this.openState === constants.OPENED || this.openState === constants.REOPENED;
},
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
@@ -117,6 +116,9 @@ export default {
endpoint() {
return this.getNoteableData.create_note_path;
},
+ issuableTypeTitle() {
+ return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE ? 'merge request' : 'issue';
+ },
},
watch: {
note(newNote) {
@@ -129,9 +131,7 @@ export default {
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
- this.toggleIssueLocalState(
- isClosed ? constants.CLOSED : constants.REOPENED,
- );
+ this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
});
this.initAutoSave();
@@ -168,6 +168,7 @@ export default {
noteable_id: this.getNoteableData.id,
note: this.note,
},
+ merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
},
};
@@ -227,9 +228,7 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false);
Flash(
sprintf(
- __(
- 'Something went wrong while closing the %{issuable}. Please try again later',
- ),
+ __('Something went wrong while closing the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
),
);
@@ -242,9 +241,7 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false);
Flash(
sprintf(
- __(
- 'Something went wrong while reopening the %{issuable}. Please try again later',
- ),
+ __('Something went wrong while reopening the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
),
);
@@ -281,9 +278,7 @@ Please check your network connection and try again.`;
},
initAutoSave() {
if (this.isLoggedIn) {
- const noteableType = capitalizeFirstCharacter(
- convertToCamelCase(this.noteableType),
- );
+ const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
this.autosave = new Autosave($(this.$refs.textarea), [
'Note',
@@ -312,8 +307,8 @@ Please check your network connection and try again.`;
<div>
<note-signed-out-widget v-if="!isLoggedIn" />
<discussion-locked-widget
- v-else-if="isLocked(getNoteableData) && !canCreateNote"
- issuable-type="issue"
+ v-else-if="!canCreateNote"
+ :issuable-type="issuableTypeTitle"
/>
<ul
v-else-if="canCreateNote"
@@ -357,7 +352,7 @@ Please check your network connection and try again.`;
v-model="note"
:disabled="isSubmitting"
name="note[note]"
- class="note-textarea js-vue-comment-form
+ class="note-textarea js-vue-comment-form js-note-text
js-gfm-input js-autosize markdown-area js-vue-textarea"
data-supports-quick-actions="true"
aria-label="Description"
@@ -423,7 +418,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<div class="description">
<strong>Start discussion</strong>
<p>
- Discuss a specific suggestion or question.
+ {{ startDiscussionDescription }}
</p>
</div>
</button>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index cafb28910eb..d321f2ce15e 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,13 +1,15 @@
<script>
-import $ from 'jquery';
-import syntaxHighlight from '~/syntax_highlight';
+import { mapState, mapActions } from 'vuex';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import DiffFileHeader from './diff_file_header.vue';
+import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
export default {
components: {
DiffFileHeader,
+ SkeletonLoadingContainer,
},
props: {
discussion: {
@@ -15,7 +17,24 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ error: false,
+ };
+ },
computed: {
+ ...mapState({
+ noteableData: state => state.notes.noteableData,
+ }),
+ hasTruncatedDiffLines() {
+ return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
+ },
+ isDiscussionsExpanded() {
+ return true; // TODO: @fatihacet - Fix this.
+ },
+ isCollapsed() {
+ return this.diffFile.collapsed || false;
+ },
isImageDiff() {
return !this.diffFile.text;
},
@@ -23,36 +42,46 @@ export default {
const { text } = this.diffFile;
return text ? 'text-file' : 'js-image-file';
},
- diffRows() {
- return $(this.discussion.truncatedDiffLines);
- },
diffFile() {
- return convertObjectPropsToCamelCase(this.discussion.diffFile);
+ return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true });
},
imageDiffHtml() {
return this.discussion.imageDiffHtml;
},
+ currentUser() {
+ return this.noteableData.current_user;
+ },
+ userColorScheme() {
+ return window.gon.user_color_scheme;
+ },
+ normalizedDiffLines() {
+ const lines = this.discussion.truncatedDiffLines || [];
+
+ return lines.map(line => trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)));
+ },
},
mounted() {
if (this.isImageDiff) {
const canCreateNote = false;
const renderCommentBadge = true;
- imageDiffHelper.initImageDiff(
- this.$refs.fileHolder,
- canCreateNote,
- renderCommentBadge,
- );
- } else {
- const fileHolder = $(this.$refs.fileHolder);
- this.$nextTick(() => {
- syntaxHighlight(fileHolder);
- });
+ imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
+ } else if (!this.hasTruncatedDiffLines) {
+ this.fetchDiff();
}
},
methods: {
+ ...mapActions(['fetchDiscussionDiffLines']),
rowTag(html) {
return html.outerHTML ? 'tr' : 'template';
},
+ fetchDiff() {
+ this.error = false;
+ this.fetchDiscussionDiffLines(this.discussion)
+ .then(this.highlight)
+ .catch(() => {
+ this.error = true;
+ });
+ },
},
};
</script>
@@ -63,23 +92,59 @@ export default {
:class="diffFileClass"
class="diff-file file-holder"
>
- <div class="js-file-title file-title file-title-flex-parent">
- <diff-file-header
- :diff-file="diffFile"
- />
- </div>
+ <diff-file-header
+ :diff-file="diffFile"
+ :current-user="currentUser"
+ :discussions-expanded="isDiscussionsExpanded"
+ :expanded="!isCollapsed"
+ />
<div
v-if="diffFile.text"
- class="diff-content code js-syntax-highlight"
+ :class="userColorScheme"
+ class="diff-content code"
>
<table>
- <component
- v-for="(html, index) in diffRows"
- :is="rowTag(html)"
- :class="html.className"
- :key="index"
- v-html="html.outerHTML"
- />
+ <tr
+ v-for="line in normalizedDiffLines"
+ :key="line.lineCode"
+ class="line_holder"
+ >
+ <td class="diff-line-num old_line">{{ line.oldLine }}</td>
+ <td class="diff-line-num new_line">{{ line.newLine }}</td>
+ <td
+ :class="line.type"
+ class="line_content"
+ v-html="line.richText"
+ >
+ </td>
+ </tr>
+ <tr
+ v-if="!hasTruncatedDiffLines"
+ class="line_holder line-holder-placeholder"
+ >
+ <td class="old_line diff-line-num"></td>
+ <td class="new_line diff-line-num"></td>
+ <td
+ v-if="error"
+ class="js-error-lazy-load-diff diff-loading-error-block"
+ >
+ Unable to load the diff
+ <button
+ class="btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button"
+ @click="fetchDiff"
+ >
+ Try again
+ </button>
+ </td>
+ <td
+ v-else
+ class="line_content js-success-lazy-load"
+ >
+ <span></span>
+ <skeleton-loading-container />
+ <span></span>
+ </td>
+ </tr>
<tr class="notes_holder">
<td
class="notes_line"
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 68e17ac8055..6385b75e557 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg';
@@ -48,10 +48,14 @@ export default {
this.nextDiscussionSvg = nextDiscussionSvg;
},
methods: {
- jumpToFirstDiscussion() {
- const el = document.querySelector(
- `[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`,
- );
+ ...mapActions(['expandDiscussion']),
+ jumpToFirstUnresolvedDiscussion() {
+ const discussionId = this.firstUnresolvedDiscussionId;
+ if (!discussionId) {
+ return;
+ }
+
+ const el = document.querySelector(`[data-discussion-id="${discussionId}"]`);
const activeTab = window.mrTabs.currentAction;
if (activeTab === 'commits' || activeTab === 'pipelines') {
@@ -59,6 +63,7 @@ export default {
}
if (el) {
+ this.expandDiscussion({ discussionId });
scrollToElement(el);
}
},
@@ -97,7 +102,7 @@ export default {
<a
v-tooltip
:href="resolveAllDiscussionsIssuePath"
- title="Resolve all discussions in new issue"
+ :title="s__('Resolve all discussions in new issue')"
data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn">
<span v-html="mrIssueSvg"></span>
@@ -112,7 +117,7 @@ export default {
title="Jump to first unresolved discussion"
data-container="body"
class="btn btn-default discussion-next-btn"
- @click="jumpToFirstDiscussion">
+ @click="jumpToFirstUnresolvedDiscussion">
<span v-html="nextDiscussionSvg"></span>
</button>
</div>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 0bf4258a257..cdbbb342331 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -27,6 +27,10 @@ export default {
type: Number,
required: true,
},
+ noteUrl: {
+ type: String,
+ required: true,
+ },
accessLevel: {
type: String,
required: false,
@@ -48,6 +52,11 @@ export default {
type: Boolean,
required: true,
},
+ canResolve: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
resolvable: {
type: Boolean,
required: false,
@@ -125,7 +134,7 @@ export default {
{{ accessLevel }}
</span>
<div
- v-if="resolvable"
+ v-if="canResolve"
class="note-actions-item">
<button
v-tooltip
@@ -216,6 +225,15 @@ export default {
Report as abuse
</a>
</li>
+ <li>
+ <button
+ :data-clipboard-text="noteUrl"
+ type="button"
+ css-class="btn-default btn-transparent"
+ >
+ Copy link
+ </button>
+ </li>
<li v-if="canEdit">
<button
class="btn btn-transparent js-note-delete js-note-delete"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 864edcd2ec6..d2db68df98e 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -40,7 +40,7 @@ export default {
this.initTaskList();
if (this.isEditing) {
- this.initAutoSave(this.note.noteable_type);
+ this.initAutoSave(this.note);
}
},
updated() {
@@ -49,7 +49,7 @@ export default {
if (this.isEditing) {
if (!this.autosave) {
- this.initAutoSave(this.note.noteable_type);
+ this.initAutoSave(this.note);
} else {
this.setAutoSave();
}
@@ -72,7 +72,7 @@ export default {
this.$emit('handleFormUpdate', note, parentElement, callback);
},
formCancelHandler(shouldConfirm, isDirty) {
- this.$emit('cancelFormEdition', shouldConfirm, isDirty);
+ this.$emit('cancelForm', shouldConfirm, isDirty);
},
},
};
@@ -93,7 +93,7 @@ export default {
:note-body="noteBody"
:note-id="note.id"
@handleFormUpdate="handleFormUpdate"
- @cancelFormEdition="formCancelHandler"
+ @cancelForm="formCancelHandler"
/>
<textarea
v-if="canEdit"
@@ -105,6 +105,7 @@ export default {
:edited-at="note.last_edited_at"
:edited-by="note.last_edited_by"
action-text="Edited"
+ class="note_edited_ago"
/>
<note-awards-list
v-if="note.award_emoji.length"
diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue
index 2dc39d1a186..391bb2ae179 100644
--- a/app/assets/javascripts/notes/components/note_edited_text.vue
+++ b/app/assets/javascripts/notes/components/note_edited_text.vue
@@ -11,14 +11,20 @@ export default {
type: String,
required: true,
},
+ actionDetailText: {
+ type: String,
+ required: false,
+ default: '',
+ },
editedAt: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
editedBy: {
type: Object,
required: false,
- default: () => ({}),
+ default: null,
},
className: {
type: String,
@@ -33,13 +39,14 @@ export default {
<div :class="className">
{{ actionText }}
<template v-if="editedBy">
- {{ s__('ByAuthor|by') }}
+ by
<a
:href="editedBy.path"
class="js-vue-author author_link">
{{ editedBy.name }}
</a>
</template>
+ {{ actionDetailText }}
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 7254ef3357d..a4e3faa5d75 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -29,7 +29,7 @@ export default {
required: false,
default: 'Save comment',
},
- note: {
+ discussion: {
type: Object,
required: false,
default: () => ({}),
@@ -38,6 +38,11 @@ export default {
type: Boolean,
required: true,
},
+ lineCode: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -66,9 +71,7 @@ export default {
return this.getNotesDataByProp('markdownDocsPath');
},
quickActionsDocsPath() {
- return !this.isEditing
- ? this.getNotesDataByProp('quickActionsDocsPath')
- : undefined;
+ return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined;
},
currentUserId() {
return this.getUserDataByProp('id');
@@ -95,24 +98,17 @@ export default {
const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true;
- this.$emit(
- 'handleFormUpdate',
- this.updatedNoteBody,
- this.$refs.editNoteForm,
- () => {
- this.isSubmitting = false;
+ this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => {
+ this.isSubmitting = false;
- if (shouldResolve) {
- this.resolveHandler(beforeSubmitDiscussionState);
- }
- },
- );
+ if (shouldResolve) {
+ this.resolveHandler(beforeSubmitDiscussionState);
+ }
+ });
},
editMyLastNote() {
if (this.updatedNoteBody === '') {
- const lastNoteInDiscussion = this.getDiscussionLastNote(
- this.updatedNoteBody,
- );
+ const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion);
if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', {
@@ -123,11 +119,7 @@ export default {
},
cancelHandler(shouldConfirm = false) {
// Sends information about confirm message and if the textarea has changed
- this.$emit(
- 'cancelFormEdition',
- shouldConfirm,
- this.noteBody !== this.updatedNoteBody,
- );
+ this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody);
},
},
};
@@ -136,7 +128,7 @@ export default {
<template>
<div
ref="editNoteForm"
- class="note-edit-form current-note-edit-form">
+ class="note-edit-form current-note-edit-form js-discussion-note-form">
<div
v-if="conflictWhileEditing"
class="js-conflict-edit-warning alert alert-danger">
@@ -150,7 +142,10 @@ export default {
to ensure information is not lost.
</div>
<div class="flash-container timeline-content"></div>
- <form class="edit-note common-note-form js-quick-submit gfm-form">
+ <form
+ :data-line-code="lineCode"
+ class="edit-note common-note-form js-quick-submit gfm-form"
+ >
<issue-warning
v-if="hasWarning(getNoteableData)"
@@ -170,7 +165,7 @@ export default {
:data-supports-quick-actions="!isEditing"
v-model="updatedNoteBody"
name="note[note]"
- class="note-textarea js-gfm-input
+ class="note-textarea js-gfm-input js-note-text
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
@@ -184,22 +179,22 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
<button
:disabled="isDisabled"
type="button"
- class="js-vue-issue-save btn btn-save"
+ class="js-vue-issue-save btn btn-save js-comment-button "
@click="handleUpdate()">
{{ saveButtonTitle }}
</button>
<button
- v-if="note.resolvable"
+ v-if="discussion.resolvable"
class="btn btn-nr btn-default append-right-10 js-comment-resolve-button"
@click.prevent="handleUpdate(true)"
>
{{ resolveButtonTitle }}
</button>
<button
- class="btn btn-cancel note-edit-cancel"
+ class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
type="button"
@click="cancelHandler()">
- Cancel
+ {{ __('Discard draft') }}
</button>
</div>
</form>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index ffe3ba9c805..ee3580895df 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -20,11 +20,6 @@ export default {
required: false,
default: '',
},
- actionTextHtml: {
- type: String,
- required: false,
- default: '',
- },
noteId: {
type: Number,
required: true,
@@ -88,10 +83,8 @@ export default {
<template v-if="actionText">
{{ actionText }}
</template>
- <span
- v-if="actionTextHtml"
- class="system-note-message"
- v-html="actionTextHtml">
+ <span class="system-note-message">
+ <slot></slot>
</span>
<span class="system-note-separator">
&middot;
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 3f865431155..bee635398b3 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,7 +1,11 @@
<script>
+import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg';
+import { convertObjectPropsToCamelCase, scrollToElement } from '~/lib/utils/common_utils';
+import { truncateSha } from '~/lib/utils/text_utility';
+import systemNote from '~/vue_shared/components/notes/system_note.vue';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -17,9 +21,9 @@ import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import tooltip from '../../vue_shared/directives/tooltip';
-import { scrollToElement } from '../../lib/utils/common_utils';
export default {
+ name: 'NoteableDiscussion',
components: {
noteableNote,
diffWithNote,
@@ -30,16 +34,32 @@ export default {
noteForm,
placeholderNote,
placeholderSystemNote,
+ systemNote,
},
directives: {
tooltip,
},
mixins: [autosave, noteable, resolvable],
props: {
- note: {
+ discussion: {
type: Object,
required: true,
},
+ renderHeader: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ renderDiffFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ alwaysExpanded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -53,19 +73,27 @@ export default {
'getNoteableData',
'discussionCount',
'resolvedDiscussionCount',
+ 'allDiscussions',
'unresolvedDiscussions',
]),
- discussion() {
+ transformedDiscussion() {
return {
- ...this.note.notes[0],
- truncatedDiffLines: this.note.truncated_diff_lines,
- diffFile: this.note.diff_file,
- diffDiscussion: this.note.diff_discussion,
- imageDiffHtml: this.note.image_diff_html,
+ ...this.discussion.notes[0],
+ truncatedDiffLines: this.discussion.truncated_diff_lines || [],
+ truncatedDiffLinesPath: this.discussion.truncated_diff_lines_path,
+ diffFile: this.discussion.diff_file,
+ diffDiscussion: this.discussion.diff_discussion,
+ imageDiffHtml: this.discussion.image_diff_html,
+ active: this.discussion.active,
+ discussionPath: this.discussion.discussion_path,
+ resolved: this.discussion.resolved,
+ resolvedBy: this.discussion.resolved_by,
+ resolvedByPush: this.discussion.resolved_by_push,
+ resolvedAt: this.discussion.resolved_at,
};
},
author() {
- return this.discussion.author;
+ return this.transformedDiscussion.author;
},
canReply() {
return this.getNoteableData.current_user.can_create_note;
@@ -74,7 +102,7 @@ export default {
return this.getNoteableData.create_note_path;
},
lastUpdatedBy() {
- const { notes } = this.note;
+ const { notes } = this.discussion;
if (notes.length > 1) {
return notes[notes.length - 1].author;
@@ -83,7 +111,7 @@ export default {
return null;
},
lastUpdatedAt() {
- const { notes } = this.note;
+ const { notes } = this.discussion;
if (notes.length > 1) {
return notes[notes.length - 1].created_at;
@@ -91,27 +119,40 @@ export default {
return null;
},
- hasUnresolvedDiscussion() {
- return this.unresolvedDiscussions.length > 0;
+ resolvedText() {
+ return this.transformedDiscussion.resolvedByPush ? 'Automatically resolved' : 'Resolved';
+ },
+ hasMultipleUnresolvedDiscussions() {
+ return this.unresolvedDiscussions.length > 1;
+ },
+ shouldRenderDiffs() {
+ const { diffDiscussion, diffFile } = this.transformedDiscussion;
+
+ return diffDiscussion && diffFile && this.renderDiffFile;
},
wrapperComponent() {
- return this.discussion.diffDiscussion && this.discussion.diffFile
- ? diffWithNote
- : 'div';
+ return this.shouldRenderDiffs ? diffWithNote : 'div';
+ },
+ wrapperComponentProps() {
+ if (this.shouldRenderDiffs) {
+ return { discussion: convertObjectPropsToCamelCase(this.discussion) };
+ }
+
+ return {};
},
wrapperClass() {
- return this.isDiffDiscussion ? '' : 'card';
+ return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
},
},
mounted() {
if (this.isReplying) {
- this.initAutoSave(this.discussion.noteable_type);
+ this.initAutoSave(this.transformedDiscussion);
}
},
updated() {
if (this.isReplying) {
if (!this.autosave) {
- this.initAutoSave(this.discussion.noteable_type);
+ this.initAutoSave(this.transformedDiscussion);
} else {
this.setAutoSave();
}
@@ -127,7 +168,9 @@ export default {
'toggleDiscussion',
'removePlaceholderNotes',
'toggleResolveNote',
+ 'expandDiscussion',
]),
+ truncateSha,
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
@@ -136,23 +179,25 @@ export default {
return placeholderNote;
}
+ if (note.system) {
+ return systemNote;
+ }
+
return noteableNote;
},
componentData(note) {
- return note.isPlaceholderNote ? this.note.notes[0] : note;
+ return note.isPlaceholderNote ? this.discussion.notes[0] : note;
},
toggleDiscussionHandler() {
- this.toggleDiscussion({ discussionId: this.note.id });
+ this.toggleDiscussion({ discussionId: this.discussion.id });
},
showReplyForm() {
this.isReplying = true;
},
cancelReplyForm(shouldConfirm) {
if (shouldConfirm && this.$refs.noteForm.isDirty) {
- const msg = 'Are you sure you want to cancel creating this comment?';
-
// eslint-disable-next-line no-alert
- if (!window.confirm(msg)) {
+ if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
return;
}
}
@@ -161,18 +206,23 @@ export default {
this.isReplying = false;
},
saveReply(noteText, form, callback) {
+ const postData = {
+ in_reply_to_discussion_id: this.discussion.reply_id,
+ target_type: this.getNoteableData.targetType,
+ note: { note: noteText },
+ };
+
+ if (this.discussion.for_commit) {
+ postData.note_project_id = this.discussion.project_id;
+ }
+
const replyData = {
endpoint: this.newNotePath,
flashContainer: this.$el,
- data: {
- in_reply_to_discussion_id: this.note.reply_id,
- target_type: this.noteableType,
- target_id: this.discussion.noteable_id,
- note: { note: noteText },
- },
+ data: postData,
};
- this.isReplying = false;
+ this.isReplying = false;
this.saveNote(replyData)
.then(() => {
this.resetAutoSave();
@@ -190,15 +240,19 @@ Please check your network connection and try again.`;
});
});
},
- jumpToDiscussion() {
+ jumpToNextDiscussion() {
+ const discussionIds = this.allDiscussions.map(d => d.id);
const unresolvedIds = this.unresolvedDiscussions.map(d => d.id);
- const index = unresolvedIds.indexOf(this.note.id);
+ const currentIndex = discussionIds.indexOf(this.discussion.id);
+ const remainingAfterCurrent = discussionIds.slice(currentIndex + 1);
+ const nextIndex = _.findIndex(remainingAfterCurrent, id => unresolvedIds.indexOf(id) > -1);
- if (index >= 0 && index !== unresolvedIds.length) {
- const nextId = unresolvedIds[index + 1];
+ if (nextIndex > -1) {
+ const nextId = remainingAfterCurrent[nextIndex];
const el = document.querySelector(`[data-discussion-id="${nextId}"]`);
if (el) {
+ this.expandDiscussion({ discussionId: nextId });
scrollToElement(el);
}
}
@@ -208,9 +262,7 @@ Please check your network connection and try again.`;
</script>
<template>
- <li
- :data-discussion-id="note.id"
- class="note note-discussion timeline-entry">
+ <li class="note note-discussion timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
@@ -221,20 +273,52 @@ Please check your network connection and try again.`;
/>
</div>
<div class="timeline-content">
- <div class="discussion">
- <div class="discussion-header">
+ <div
+ :data-discussion-id="transformedDiscussion.discussion_id"
+ class="discussion js-discussion-container"
+ >
+ <div
+ v-if="renderHeader"
+ class="discussion-header"
+ >
<note-header
:author="author"
- :created-at="discussion.created_at"
- :note-id="discussion.id"
+ :created-at="transformedDiscussion.created_at"
+ :note-id="transformedDiscussion.id"
:include-toggle="true"
- :expanded="note.expanded"
- action-text="started a discussion"
- class="discussion"
+ :expanded="discussion.expanded"
@toggleHandler="toggleDiscussionHandler"
+ >
+ <template v-if="transformedDiscussion.diffDiscussion">
+ started a discussion on
+ <a :href="transformedDiscussion.discussionPath">
+ <template v-if="transformedDiscussion.active">
+ the diff
+ </template>
+ <template v-else>
+ an old version of the diff
+ </template>
+ </a>
+ </template>
+ <template v-else-if="discussion.for_commit">
+ started a discussion on commit
+ <a :href="discussion.discussion_path">
+ {{ truncateSha(discussion.commit_id) }}
+ </a>
+ </template>
+ <template v-else>
+ started a discussion
+ </template>
+ </note-header>
+ <note-edited-text
+ v-if="transformedDiscussion.resolved"
+ :edited-at="transformedDiscussion.resolvedAt"
+ :edited-by="transformedDiscussion.resolvedBy"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline"
/>
<note-edited-text
- v-if="lastUpdatedAt"
+ v-else-if="lastUpdatedAt"
:edited-at="lastUpdatedAt"
:edited-by="lastUpdatedBy"
action-text="Last updated"
@@ -242,17 +326,17 @@ Please check your network connection and try again.`;
/>
</div>
<div
- v-if="note.expanded"
+ v-if="discussion.expanded || alwaysExpanded"
class="discussion-body">
<component
:is="wrapperComponent"
- :discussion="discussion"
+ v-bind="wrapperComponentProps"
:class="wrapperClass"
>
<div class="discussion-notes">
<ul class="notes">
<component
- v-for="note in note.notes"
+ v-for="note in discussion.notes"
:is="componentName(note)"
:note="componentData(note)"
:key="note.id"
@@ -260,27 +344,28 @@ Please check your network connection and try again.`;
</ul>
<div
:class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder">
+ class="discussion-reply-holder"
+ >
<template v-if="!isReplying && canReply">
<div
class="btn-group d-flex discussion-with-resolve-btn"
role="group">
<div
- class="btn-group"
+ class="btn-group w-100"
role="group">
<button
type="button"
- class="js-vue-discussion-reply btn btn-text-field"
+ class="js-vue-discussion-reply btn btn-text-field mr-2"
title="Add a reply"
@click="showReplyForm">Reply...</button>
</div>
<div
- v-if="note.resolvable"
+ v-if="discussion.resolvable"
class="btn-group"
role="group">
<button
type="button"
- class="btn btn-default"
+ class="btn btn-default mr-2"
@click="resolveHandler()"
>
<i
@@ -292,7 +377,7 @@ Please check your network connection and try again.`;
</button>
</div>
<div
- v-if="note.resolvable"
+ v-if="discussion.resolvable"
class="btn-group discussion-actions"
role="group"
>
@@ -302,17 +387,17 @@ Please check your network connection and try again.`;
role="group">
<a
v-tooltip
- :href="note.resolve_with_issue_path"
+ :href="discussion.resolve_with_issue_path"
+ :title="s__('MergeRequests|Resolve this discussion in a new issue')"
class="new-issue-for-discussion btn
btn-default discussion-create-issue-btn"
- title="Resolve this discussion in a new issue"
data-container="body"
>
<span v-html="resolveDiscussionsSvg"></span>
</a>
</div>
<div
- v-if="hasUnresolvedDiscussion"
+ v-if="hasMultipleUnresolvedDiscussions"
class="btn-group"
role="group">
<button
@@ -320,7 +405,7 @@ Please check your network connection and try again.`;
class="btn btn-default discussion-next-btn"
title="Jump to next unresolved discussion"
data-container="body"
- @click="jumpToDiscussion"
+ @click="jumpToNextDiscussion"
>
<span v-html="nextDiscussionsSvg"></span>
</button>
@@ -331,11 +416,11 @@ Please check your network connection and try again.`;
<note-form
v-if="isReplying"
ref="noteForm"
- :note="note"
+ :discussion="discussion"
:is-editing="false"
save-button-title="Comment"
@handleFormUpdate="saveReply"
- @cancelFormEdition="cancelReplyForm" />
+ @cancelForm="cancelReplyForm" />
<note-signed-out-widget v-if="!canReply" />
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 9225a6b1a7c..4ebeb5599f2 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -12,6 +12,7 @@ import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
export default {
+ name: 'NoteableNote',
components: {
userAvatarLink,
noteHeader,
@@ -34,26 +35,31 @@ export default {
};
},
computed: {
- ...mapGetters(['targetNoteHash', 'getUserData']),
+ ...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData']),
author() {
return this.note.author;
},
classNameBindings() {
return {
+ [`note-row-${this.note.id}`]: true,
'is-editing': this.isEditing && !this.isRequesting,
'is-requesting being-posted': this.isRequesting,
'disabled-content': this.isDeleting,
- target: this.targetNoteHash === this.noteAnchorId,
+ target: this.isTarget,
};
},
+ canResolve() {
+ return this.note.resolvable && !!this.getUserData.id;
+ },
canReportAsAbuse() {
- return (
- this.note.report_abuse_path && this.author.id !== this.getUserData.id
- );
+ return this.note.report_abuse_path && this.author.id !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
},
+ isTarget() {
+ return this.targetNoteHash === this.noteAnchorId;
+ },
},
created() {
@@ -65,13 +71,14 @@ export default {
});
},
+ mounted() {
+ if (this.isTarget) {
+ this.scrollToNoteIfNeeded($(this.$el));
+ }
+ },
+
methods: {
- ...mapActions([
- 'deleteNote',
- 'updateNote',
- 'toggleResolveNote',
- 'scrollToNoteIfNeeded',
- ]),
+ ...mapActions(['deleteNote', 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded']),
editHandler() {
this.isEditing = true;
},
@@ -85,9 +92,7 @@ export default {
this.isDeleting = false;
})
.catch(() => {
- Flash(
- 'Something went wrong while deleting your note. Please try again.',
- );
+ Flash('Something went wrong while deleting your note. Please try again.');
this.isDeleting = false;
});
}
@@ -96,7 +101,7 @@ export default {
const data = {
endpoint: this.note.path,
note: {
- target_type: this.noteableType,
+ target_type: this.getNoteableData.targetType,
target_id: this.note.noteable_id,
note: { note: noteText },
},
@@ -118,8 +123,7 @@ export default {
this.isRequesting = false;
this.isEditing = true;
this.$nextTick(() => {
- const msg =
- 'Something went wrong while editing your comment. Please try again.';
+ const msg = 'Something went wrong while editing your comment. Please try again.';
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
@@ -129,8 +133,7 @@ export default {
formCancelHandler(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert
- if (!window.confirm('Are you sure you want to cancel editing this comment?'))
- return;
+ if (!window.confirm('Are you sure you want to cancel editing this comment?')) return;
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
@@ -143,7 +146,7 @@ export default {
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
- this.$refs.noteBody.$refs.noteForm.note.note = noteText;
+ this.$refs.noteBody.note.note = noteText;
},
},
};
@@ -154,7 +157,9 @@ export default {
:id="noteAnchorId"
:class="classNameBindings"
:data-award-url="note.toggle_award_path"
- class="note timeline-entry">
+ :data-note-id="note.id"
+ class="note timeline-entry"
+ >
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
@@ -174,11 +179,13 @@ export default {
<note-actions
:author-id="author.id"
:note-id="note.id"
+ :note-url="note.noteable_note_url"
:access-level="note.human_access"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
+ :can-resolve="note.current_user.can_resolve"
:report-abuse-path="note.report_abuse_path"
:resolvable="note.resolvable"
:is-resolved="note.resolved"
@@ -195,7 +202,7 @@ export default {
:can-edit="note.current_user.can_edit"
:is-editing="isEditing"
@handleFormUpdate="formUpdateHandler"
- @cancelFormEdition="formCancelHandler"
+ @cancelForm="formCancelHandler"
/>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index ebfc827ac57..98f8b9af168 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,9 +1,7 @@
<script>
-import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash';
-import store from '../stores/';
import * as constants from '../constants';
import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue';
@@ -39,19 +37,23 @@ export default {
required: false,
default: () => ({}),
},
+ shouldShow: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
- store,
data() {
return {
isLoading: true,
};
},
computed: {
- ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
+ ...mapGetters(['discussions', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
return this.noteableData.noteableType;
},
- allNotes() {
+ allDiscussions() {
if (this.isLoading) {
const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
@@ -59,36 +61,29 @@ export default {
isSkeletonNote: true,
});
}
- return this.notes;
+ return this.discussions;
},
},
created() {
this.setNotesData(this.notesData);
this.setNoteableData(this.noteableData);
this.setUserData(this.userData);
+ this.setTargetNoteHash(getLocationHash());
},
mounted() {
this.fetchNotes();
+ const { parentElement } = this.$el;
- const parentElement = this.$el.parentElement;
-
- if (
- parentElement &&
- parentElement.classList.contains('js-vue-notes-event')
- ) {
+ if (parentElement && parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', event => {
const { awardName, noteId } = event.detail;
this.actionToggleAward({ awardName, noteId });
});
}
- document.addEventListener('refreshVueNotes', this.fetchNotes);
- },
- beforeDestroy() {
- document.removeEventListener('refreshVueNotes', this.fetchNotes);
},
methods: {
...mapActions({
- actionFetchNotes: 'fetchNotes',
+ fetchDiscussions: 'fetchDiscussions',
poll: 'poll',
actionToggleAward: 'toggleAward',
scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
@@ -97,28 +92,31 @@ export default {
setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash',
+ toggleDiscussion: 'toggleDiscussion',
}),
- getComponentName(note) {
- if (note.isSkeletonNote) {
+ getComponentName(discussion) {
+ if (discussion.isSkeletonNote) {
return skeletonLoadingContainer;
}
- if (note.isPlaceholderNote) {
- if (note.placeholderType === constants.SYSTEM_NOTE) {
+ if (discussion.isPlaceholderNote) {
+ if (discussion.placeholderType === constants.SYSTEM_NOTE) {
return placeholderSystemNote;
}
return placeholderNote;
- } else if (note.individual_note) {
- return note.notes[0].system ? systemNote : noteableNote;
+ } else if (discussion.individual_note) {
+ return discussion.notes[0].system ? systemNote : noteableNote;
}
return noteableDiscussion;
},
- getComponentData(note) {
- return note.individual_note ? note.notes[0] : note;
+ getComponentData(discussion) {
+ return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
},
fetchNotes() {
- return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath'))
- .then(() => this.initPolling())
+ return this.fetchDiscussions(this.getNotesDataByProp('discussionsPath'))
+ .then(() => {
+ this.initPolling();
+ })
.then(() => {
this.isLoading = false;
})
@@ -126,9 +124,7 @@ export default {
.then(() => this.checkLocationHash())
.catch(() => {
this.isLoading = false;
- Flash(
- 'Something went wrong while fetching comments. Please try again.',
- );
+ Flash('Something went wrong while fetching comments. Please try again.');
});
},
initPolling() {
@@ -143,11 +139,19 @@ export default {
},
checkLocationHash() {
const hash = getLocationHash();
- const element = document.getElementById(hash);
+ const noteId = hash && hash.replace(/^note_/, '');
- if (hash && element) {
- this.setTargetNoteHash(hash);
- this.scrollToNoteIfNeeded($(element));
+ if (noteId) {
+ this.discussions.forEach(discussion => {
+ if (discussion.notes) {
+ discussion.notes.forEach(note => {
+ if (`${note.id}` === `${noteId}`) {
+ // FIXME: this modifies the store state without using a mutation/action
+ Object.assign(discussion, { expanded: true });
+ }
+ });
+ }
+ });
}
},
},
@@ -155,16 +159,18 @@ export default {
</script>
<template>
- <div id="notes">
+ <div
+ v-if="shouldShow"
+ id="notes">
<ul
id="notes-list"
class="notes main-notes-list timeline">
<component
- v-for="note in allNotes"
- :is="getComponentName(note)"
- :note="getComponentData(note)"
- :key="note.id"
+ v-for="discussion in allDiscussions"
+ :is="getComponentName(discussion)"
+ v-bind="getComponentData(discussion)"
+ :key="discussion.id"
/>
</ul>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index 5b5b1e89058..2c3e07c0506 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -11,7 +11,7 @@ export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const ISSUE_NOTEABLE_TYPE = 'issue';
export const EPIC_NOTEABLE_TYPE = 'epic';
-export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
+export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description';
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index e4121f151db..eed3a82854d 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,46 +1,49 @@
import Vue from 'vue';
import notesApp from './components/notes_app.vue';
+import createStore from './stores';
-document.addEventListener(
- 'DOMContentLoaded',
- () =>
- new Vue({
- el: '#js-vue-notes',
- components: {
- notesApp,
- },
- data() {
- const notesDataset = document.getElementById('js-vue-notes').dataset;
- const parsedUserData = JSON.parse(notesDataset.currentUserData);
- const noteableData = JSON.parse(notesDataset.noteableData);
- let currentUserData = {};
+document.addEventListener('DOMContentLoaded', () => {
+ const store = createStore();
- noteableData.noteableType = notesDataset.noteableType;
+ return new Vue({
+ el: '#js-vue-notes',
+ components: {
+ notesApp,
+ },
+ store,
+ data() {
+ const notesDataset = document.getElementById('js-vue-notes').dataset;
+ const parsedUserData = JSON.parse(notesDataset.currentUserData);
+ const noteableData = JSON.parse(notesDataset.noteableData);
+ let currentUserData = {};
- if (parsedUserData) {
- currentUserData = {
- id: parsedUserData.id,
- name: parsedUserData.name,
- username: parsedUserData.username,
- avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
- path: parsedUserData.path,
- };
- }
+ noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
- return {
- noteableData,
- currentUserData,
- notesData: JSON.parse(notesDataset.notesData),
+ if (parsedUserData) {
+ currentUserData = {
+ id: parsedUserData.id,
+ name: parsedUserData.name,
+ username: parsedUserData.username,
+ avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
+ path: parsedUserData.path,
};
- },
- render(createElement) {
- return createElement('notes-app', {
- props: {
- noteableData: this.noteableData,
- notesData: this.notesData,
- userData: this.currentUserData,
- },
- });
- },
- }),
-);
+ }
+
+ return {
+ noteableData,
+ currentUserData,
+ notesData: JSON.parse(notesDataset.notesData),
+ };
+ },
+ render(createElement) {
+ return createElement('notes-app', {
+ props: {
+ noteableData: this.noteableData,
+ notesData: this.notesData,
+ userData: this.currentUserData,
+ },
+ });
+ },
+ });
+});
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index 3dff715905f..36cc8d5d056 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -4,11 +4,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default {
methods: {
- initAutoSave(noteableType) {
+ initAutoSave(noteable) {
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [
'Note',
- capitalizeFirstCharacter(noteableType),
- this.note.id,
+ capitalizeFirstCharacter(noteable.noteable_type),
+ noteable.id,
]);
},
resetAutoSave() {
diff --git a/app/assets/javascripts/notes/mixins/noteable.js b/app/assets/javascripts/notes/mixins/noteable.js
index b68543d71c8..bf1cd6fe5a8 100644
--- a/app/assets/javascripts/notes/mixins/noteable.js
+++ b/app/assets/javascripts/notes/mixins/noteable.js
@@ -1,15 +1,10 @@
import * as constants from '../constants';
export default {
- props: {
- note: {
- type: Object,
- required: true,
- },
- },
computed: {
noteableType() {
- return constants.NOTEABLE_TYPE_MAPPING[this.note.noteable_type];
+ const note = this.discussion ? this.discussion.notes[0] : this.note;
+ return constants.NOTEABLE_TYPE_MAPPING[note.noteable_type];
},
},
};
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index f79049b85f6..cd8394e0619 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -2,42 +2,39 @@ import Flash from '~/flash';
import { __ } from '~/locale';
export default {
- props: {
- note: {
- type: Object,
- required: true,
- },
- },
computed: {
discussionResolved() {
- const { notes, resolved } = this.note;
+ if (this.discussion) {
+ const { notes, resolved } = this.discussion;
+
+ if (notes) {
+ // Decide resolved state using store. Only valid for discussions.
+ return notes.filter(note => !note.system).every(note => note.resolved);
+ }
- if (notes) {
- // Decide resolved state using store. Only valid for discussions.
- return notes.every(note => note.resolved && !note.system);
+ return resolved;
}
- return resolved;
+ return this.note.resolved;
},
resolveButtonTitle() {
if (this.updatedNoteBody) {
if (this.discussionResolved) {
- return __('Comment and unresolve discussion');
+ return __('Comment & unresolve discussion');
}
- return __('Comment and resolve discussion');
+ return __('Comment & resolve discussion');
}
- return this.discussionResolved
- ? __('Unresolve discussion')
- : __('Resolve discussion');
+
+ return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion');
},
},
methods: {
resolveHandler(resolvedState = false) {
this.isResolving = true;
- const endpoint = this.note.resolve_path || `${this.note.path}/resolve`;
const isResolved = this.discussionResolved || resolvedState;
const discussion = this.resolveAsThread;
+ const endpoint = discussion ? this.discussion.resolve_path : `${this.note.path}/resolve`;
this.toggleResolveNote({ endpoint, isResolved, discussion })
.then(() => {
@@ -45,9 +42,8 @@ export default {
})
.catch(() => {
this.isResolving = false;
- const msg = __(
- 'Something went wrong while resolving this discussion. Please try again.',
- );
+
+ const msg = __('Something went wrong while resolving this discussion. Please try again.');
Flash(msg, 'alert', this.$el);
});
},
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index 7c623aac6ed..f5dce94caad 100644
--- a/app/assets/javascripts/notes/services/notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
@@ -5,7 +5,7 @@ import * as constants from '../constants';
Vue.use(VueResource);
export default {
- fetchNotes(endpoint) {
+ fetchDiscussions(endpoint) {
return Vue.http.get(endpoint);
},
deleteNote(endpoint) {
@@ -22,15 +22,13 @@ export default {
},
toggleResolveNote(endpoint, isResolved) {
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
- const method = isResolved
- ? UNRESOLVE_NOTE_METHOD_NAME
- : RESOLVE_NOTE_METHOD_NAME;
+ const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
return Vue.http[method](endpoint);
},
poll(data = {}) {
const endpoint = data.notesData.notesPath;
- const lastFetchedAt = data.lastFetchedAt;
+ const { lastFetchedAt } = data;
const options = {
headers: {
'X-Last-Fetched-At': lastFetchedAt ? `${lastFetchedAt}` : undefined,
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index b2222476924..0a40b48257f 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import axios from '~/lib/utils/axios_utils';
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
@@ -12,20 +13,29 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll;
+export const expandDiscussion = ({ commit }, data) => commit(types.EXPAND_DISCUSSION, data);
+
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
+
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
+
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
+
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
-export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
+
+export const setInitialNotes = ({ commit }, discussions) =>
+ commit(types.SET_INITIAL_DISCUSSIONS, discussions);
+
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
+
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
-export const fetchNotes = ({ commit }, path) =>
+export const fetchDiscussions = ({ commit }, path) =>
service
- .fetchNotes(path)
+ .fetchDiscussions(path)
.then(res => res.json())
- .then(res => {
- commit(types.SET_INITIAL_NOTES, res);
+ .then(discussions => {
+ commit(types.SET_INITIAL_DISCUSSIONS, discussions);
});
export const deleteNote = ({ commit }, note) =>
@@ -121,7 +131,8 @@ export const toggleIssueLocalState = ({ commit }, newState) => {
};
export const saveNote = ({ commit, dispatch }, noteData) => {
- const { note } = noteData.data.note;
+ // For MR discussuions we need to post as `note[note]` and issue we use `note.note`.
+ const note = noteData.data['note[note]'] || noteData.data.note.note;
let placeholderText = note;
const hasQuickActions = utils.hasQuickActions(placeholderText);
const replyId = noteData.data.in_reply_to_discussion_id;
@@ -192,7 +203,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
};
-const pollSuccessCallBack = (resp, commit, state, getters) => {
+const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
if (resp.notes && resp.notes.length) {
const { notesById } = getters;
@@ -200,10 +211,12 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note);
} else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
- const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
+ const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
+ } else if (note.type === constants.DIFF_NOTE) {
+ dispatch('fetchDiscussions', state.notesData.discussionsPath);
} else {
commit(types.ADD_NEW_NOTE, note);
}
@@ -218,13 +231,13 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
return resp;
};
-export const poll = ({ commit, state, getters }) => {
+export const poll = ({ commit, state, getters, dispatch }) => {
eTagPoll = new Poll({
resource: service,
method: 'poll',
data: state,
successCallback: resp =>
- resp.json().then(data => pollSuccessCallBack(data, commit, state, getters)),
+ resp.json().then(data => pollSuccessCallBack(data, commit, state, getters, dispatch)),
errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
});
@@ -285,5 +298,13 @@ export const scrollToNoteIfNeeded = (context, el) => {
}
};
+export const fetchDiscussionDiffLines = ({ commit }, discussion) =>
+ axios.get(discussion.truncatedDiffLinesPath).then(({ data }) => {
+ commit(types.SET_DISCUSSION_DIFF_LINES, {
+ discussionId: discussion.id,
+ diffLines: data.truncated_diff_lines,
+ });
+ });
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index bc373e0d0fc..ab28bb48e9e 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,58 +1,89 @@
import _ from 'underscore';
+import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
-export const notes = state => collapseSystemNotes(state.notes);
+export const discussions = state => collapseSystemNotes(state.discussions);
export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData;
+
export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData;
+
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
+
export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
-export const getUserDataByProp = state => prop =>
- state.userData && state.userData[prop];
+
+export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
export const notesById = state =>
- state.notes.reduce((acc, note) => {
+ state.discussions.reduce((acc, note) => {
note.notes.every(n => Object.assign(acc, { [n.id]: n }));
return acc;
}, {});
+export const discussionsByLineCode = state =>
+ state.discussions.reduce((acc, note) => {
+ if (note.diff_discussion && note.line_code && note.resolvable) {
+ // For context about line notes: there might be multiple notes with the same line code
+ const items = acc[note.line_code] || [];
+ items.push(note);
+
+ Object.assign(acc, { [note.line_code]: items });
+ }
+ return acc;
+ }, {});
+
+export const noteableType = state => {
+ const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
+
+ if (state.noteableData.noteableType === EPIC_NOTEABLE_TYPE) {
+ return EPIC_NOTEABLE_TYPE;
+ }
+
+ return state.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE;
+};
+
const reverseNotes = array => array.slice(0).reverse();
+
const isLastNote = (note, state) =>
- !note.system &&
- state.userData &&
- note.author &&
- note.author.id === state.userData.id;
+ !note.system && state.userData && note.author && note.author.id === state.userData.id;
export const getCurrentUserLastNote = state =>
- _.flatten(
- reverseNotes(state.notes).map(note => reverseNotes(note.notes)),
- ).find(el => isLastNote(el, state));
+ _.flatten(reverseNotes(state.discussions).map(note => reverseNotes(note.notes))).find(el =>
+ isLastNote(el, state),
+ );
export const getDiscussionLastNote = state => discussion =>
reverseNotes(discussion.notes).find(el => isLastNote(el, state));
export const discussionCount = state => {
- const discussions = state.notes.filter(n => !n.individual_note);
+ const filteredDiscussions = state.discussions.filter(n => !n.individual_note && n.resolvable);
- return discussions.length;
+ return filteredDiscussions.length;
};
export const unresolvedDiscussions = (state, getters) => {
const resolvedMap = getters.resolvedDiscussionsById;
- return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
+ return state.discussions.filter(n => !n.individual_note && !resolvedMap[n.id]);
+};
+
+export const allDiscussions = (state, getters) => {
+ const resolved = getters.resolvedDiscussionsById;
+ const unresolved = getters.unresolvedDiscussions;
+
+ return Object.values(resolved).concat(unresolved);
};
export const resolvedDiscussionsById = state => {
const map = {};
- state.notes.forEach(n => {
+ state.discussions.forEach(n => {
if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system);
@@ -71,5 +102,15 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length;
};
+export const discussionTabCounter = state => {
+ let all = [];
+
+ state.discussions.forEach(discussion => {
+ all = all.concat(discussion.notes.filter(note => !note.system && !note.placeholder));
+ });
+
+ return all.length;
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js
index 9ed19bf171e..0f48b8880f4 100644
--- a/app/assets/javascripts/notes/stores/index.js
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -3,24 +3,14 @@ import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
+import module from './modules';
Vue.use(Vuex);
-export default new Vuex.Store({
- state: {
- notes: [],
- targetNoteHash: null,
- lastFetchedAt: null,
-
- // View layer
- isToggleStateButtonLoading: false,
-
- // holds endpoints and permissions provided through haml
- notesData: {},
- userData: {},
- noteableData: {},
- },
- actions,
- getters,
- mutations,
-});
+export default () =>
+ new Vuex.Store({
+ state: module.state,
+ actions,
+ getters,
+ mutations,
+ });
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
new file mode 100644
index 00000000000..a978490c009
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -0,0 +1,26 @@
+import * as actions from '../actions';
+import * as getters from '../getters';
+import mutations from '../mutations';
+
+export default {
+ state: {
+ discussions: [],
+ targetNoteHash: null,
+ lastFetchedAt: null,
+
+ // View layer
+ isToggleStateButtonLoading: false,
+
+ // holds endpoints and permissions provided through haml
+ notesData: {
+ markdownDocsPath: '',
+ },
+ userData: {},
+ noteableData: {
+ current_user: {},
+ },
+ },
+ actions,
+ getters,
+ mutations,
+};
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index b455e23ecde..caead4cb860 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -1,11 +1,12 @@
export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
export const DELETE_NOTE = 'DELETE_NOTE';
+export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA';
-export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES';
+export const SET_INITIAL_DISCUSSIONS = 'SET_INITIAL_DISCUSSIONS';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
@@ -13,6 +14,7 @@ export const TOGGLE_AWARD = 'TOGGLE_AWARD';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
+export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index c8edc06349f..ea165709e61 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -6,8 +6,8 @@ import { isInMRPage } from '../../lib/utils/common_utils';
export default {
[types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note;
- const [exists] = state.notes.filter(n => n.id === note.discussion_id);
- const isDiscussion = type === constants.DISCUSSION_NOTE;
+ const [exists] = state.discussions.filter(n => n.id === note.discussion_id);
+ const isDiscussion = type === constants.DISCUSSION_NOTE || type === constants.DIFF_NOTE;
if (!exists) {
const noteData = {
@@ -25,42 +25,44 @@ export default {
noteData.resolve_with_issue_path = note.resolve_with_issue_path;
}
- state.notes.push(noteData);
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
+ state.discussions.push(noteData);
}
},
[types.ADD_NEW_REPLY_TO_DISCUSSION](state, note) {
- const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+ const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj) {
noteObj.notes.push(note);
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
}
},
[types.DELETE_NOTE](state, note) {
- const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+ const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj.individual_note) {
- state.notes.splice(state.notes.indexOf(noteObj), 1);
+ state.discussions.splice(state.discussions.indexOf(noteObj), 1);
} else {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1);
if (!noteObj.notes.length) {
- state.notes.splice(state.notes.indexOf(noteObj), 1);
+ state.discussions.splice(state.discussions.indexOf(noteObj), 1);
}
}
+ },
+
+ [types.EXPAND_DISCUSSION](state, { discussionId }) {
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
+ discussion.expanded = true;
},
[types.REMOVE_PLACEHOLDER_NOTES](state) {
- const { notes } = state;
+ const { discussions } = state;
- for (let i = notes.length - 1; i >= 0; i -= 1) {
- const note = notes[i];
+ for (let i = discussions.length - 1; i >= 0; i -= 1) {
+ const note = discussions[i];
const children = note.notes;
if (children.length && !note.individual_note) {
@@ -72,7 +74,7 @@ export default {
}
} else if (note.isPlaceholderNote) {
// remove placeholders from state root
- notes.splice(i, 1);
+ discussions.splice(i, 1);
}
}
},
@@ -88,29 +90,29 @@ export default {
[types.SET_USER_DATA](state, data) {
Object.assign(state, { userData: data });
},
- [types.SET_INITIAL_NOTES](state, notesData) {
- const notes = [];
+ [types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
+ const discussions = [];
- notesData.forEach(note => {
+ discussionsData.forEach(discussion => {
// To support legacy notes, should be very rare case.
- if (note.individual_note && note.notes.length > 1) {
- note.notes.forEach(n => {
- notes.push({
- ...note,
+ if (discussion.individual_note && discussion.notes.length > 1) {
+ discussion.notes.forEach(n => {
+ discussions.push({
+ ...discussion,
notes: [n], // override notes array to only have one item to mimick individual_note
});
});
} else {
- const oldNote = utils.findNoteObjectById(state.notes, note.id);
+ const oldNote = utils.findNoteObjectById(state.discussions, discussion.id);
- notes.push({
- ...note,
- expanded: oldNote ? oldNote.expanded : note.expanded,
+ discussions.push({
+ ...discussion,
+ expanded: oldNote ? oldNote.expanded : discussion.expanded,
});
}
});
- Object.assign(state, { notes });
+ Object.assign(state, { discussions });
},
[types.SET_LAST_FETCHED_AT](state, fetchedAt) {
@@ -122,17 +124,17 @@ export default {
},
[types.SHOW_PLACEHOLDER_NOTE](state, data) {
- let notesArr = state.notes;
- if (data.replyId) {
- notesArr = utils.findNoteObjectById(notesArr, data.replyId).notes;
+ let notesArr = state.discussions;
+
+ const existingDiscussion = utils.findNoteObjectById(notesArr, data.replyId);
+ if (existingDiscussion) {
+ notesArr = existingDiscussion.notes;
}
notesArr.push({
individual_note: true,
isPlaceholderNote: true,
- placeholderType: data.isSystemNote
- ? constants.SYSTEM_NOTE
- : constants.NOTE,
+ placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE,
notes: [
{
body: data.noteBody,
@@ -151,28 +153,23 @@ export default {
if (hasEmojiAwardedByCurrentUser.length) {
// If current user has awarded this emoji, remove it.
- note.award_emoji.splice(
- note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]),
- 1,
- );
+ note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1);
} else {
note.award_emoji.push({
name: awardName,
user: { id, name, username },
});
}
-
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.TOGGLE_DISCUSSION](state, { discussionId }) {
- const discussion = utils.findNoteObjectById(state.notes, discussionId);
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
discussion.expanded = !discussion.expanded;
},
[types.UPDATE_NOTE](state, note) {
- const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+ const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj.individual_note) {
noteObj.notes.splice(0, 1, note);
@@ -180,24 +177,20 @@ export default {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
}
-
- // document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.UPDATE_DISCUSSION](state, noteData) {
const note = noteData;
let index = 0;
- state.notes.forEach((n, i) => {
+ state.discussions.forEach((n, i) => {
if (n.id === note.id) {
index = i;
}
});
note.expanded = true; // override expand flag to prevent collapse
- state.notes.splice(index, 1, note);
-
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
+ state.discussions.splice(index, 1, note);
},
[types.CLOSE_ISSUE](state) {
@@ -211,4 +204,15 @@ export default {
[types.TOGGLE_STATE_BUTTON_LOADING](state, value) {
Object.assign(state, { isToggleStateButtonLoading: value });
},
+
+ [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
+ const index = state.discussions.indexOf(discussion);
+
+ const discussionWithDiffLines = Object.assign({}, discussion, {
+ truncated_diff_lines: diffLines,
+ });
+
+ state.discussions.splice(index, 1, discussionWithDiffLines);
+ },
};
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index cc2805a1901..d6aa4bb95d2 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -96,7 +96,7 @@
this.enteredUsername = '';
},
onSecondaryAction() {
- const form = this.$refs.form;
+ const { form } = this.$refs;
form.action = this.blockUserUrl;
this.$refs.method.value = 'put';
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 6fc43af2623..ff19b9a9c30 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -61,7 +61,7 @@ export default class Todos {
e.stopPropagation();
e.preventDefault();
- const target = e.target;
+ const { target } = e;
target.setAttribute('disabled', true);
target.classList.add('disabled');
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
index ae72c8cb4d5..6c1788dc160 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
@@ -80,10 +80,11 @@ export default (function() {
};
ContributorsStatGraph.prototype.redraw_authors = function() {
- var author_commits, x_domain;
$("ol").html("");
- x_domain = ContributorsGraph.prototype.x_domain;
- author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
+
+ const { x_domain } = ContributorsGraph.prototype;
+ const author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
+
return _.each(author_commits, (function(_this) {
return function(d) {
_this.redraw_author_commit_info(d);
@@ -102,7 +103,7 @@ export default (function() {
};
ContributorsStatGraph.prototype.change_date_header = function() {
- const x_domain = ContributorsGraph.prototype.x_domain;
+ const { x_domain } = ContributorsGraph.prototype;
const formattedDateRange = sprintf(
s__('ContributorsPage|%{startDate} – %{endDate}'),
{
diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js
index 0b6c5c1d30b..9f20a3e4e46 100644
--- a/app/assets/javascripts/pages/projects/init_form.js
+++ b/app/assets/javascripts/pages/projects/init_form.js
@@ -3,5 +3,5 @@ import GLForm from '~/gl_form';
export default function ($formEl) {
new ZenMode(); // eslint-disable-line no-new
- new GLForm($formEl, true); // eslint-disable-line no-new
+ new GLForm($formEl); // eslint-disable-line no-new
}
diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js
index 14fddbc9a05..b2b8e5d2300 100644
--- a/app/assets/javascripts/pages/projects/issues/form.js
+++ b/app/assets/javascripts/pages/projects/issues/form.js
@@ -10,7 +10,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => {
new ShortcutsNavigation();
- new GLForm($('.issue-form'), true);
+ new GLForm($('.issue-form'));
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index 406fc32f9a2..3a3c21f2202 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -12,7 +12,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => {
new Diff();
new ShortcutsNavigation();
- new GLForm($('.merge-request-form'), true);
+ new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 28d8761b502..26ead75cec4 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -1,30 +1,15 @@
-import MergeRequest from '~/merge_request';
import ZenMode from '~/zen_mode';
-import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import ShortcutsIssuable from '~/shortcuts_issuable';
-import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initWidget from '../../../vue_merge_request_widget';
-export default function () {
- new Diff(); // eslint-disable-line no-new
+export default function() {
new ZenMode(); // eslint-disable-line no-new
-
initIssuableSidebar();
- initNotes();
- initDiffNotes();
initPipelines();
-
- const mrShowNode = document.querySelector('.merge-request');
-
- window.mergeRequest = new MergeRequest({
- action: mrShowNode.dataset.mrAction,
- });
-
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
howToMerge();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index e5b2827b50c..f61f4db78d5 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,4 +1,3 @@
-import { hasVueMRDiscussionsCookie } from '~/lib/utils/common_utils';
import initMrNotes from '~/mr_notes';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initShow from '../init_merge_request_show';
@@ -6,8 +5,5 @@ import initShow from '../init_merge_request_show';
document.addEventListener('DOMContentLoaded', () => {
initShow();
initSidebarBundle();
-
- if (hasVueMRDiscussionsCookie()) {
- initMrNotes();
- }
+ initMrNotes();
});
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 37ef77c8e43..1faa59fb45b 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -28,7 +28,7 @@ document.addEventListener('DOMContentLoaded', () => {
const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
autoDevOpsSettings.addEventListener('click', event => {
- const target = event.target;
+ const { target } = event;
if (target.classList.contains('js-toggle-extra-settings')) {
autoDevOpsExtraSettings.classList.toggle(
'hidden',
diff --git a/app/assets/javascripts/pages/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js
index 8d0edf7e06c..b3158f7e939 100644
--- a/app/assets/javascripts/pages/projects/tags/new/index.js
+++ b/app/assets/javascripts/pages/projects/tags/new/index.js
@@ -5,6 +5,6 @@ import GLForm from '../../../../gl_form';
document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
- new GLForm($('.tag-form'), true); // eslint-disable-line no-new
+ new GLForm($('.tag-form')); // eslint-disable-line no-new
new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index 0295653cb29..0a0fe3fc137 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
- new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
+ new GLForm($('.wiki-form')); // eslint-disable-line no-new
const deleteWikiButton = document.getElementById('delete-wiki-button');
diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
index d321892d2d2..1e7c29aefaa 100644
--- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
+++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
@@ -37,6 +37,11 @@ export default class SigninTabsMemoizer {
const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`);
if (tab) {
tab.click();
+ } else {
+ const firstTab = document.querySelector(`${this.tabSelector} a`);
+ if (firstTab) {
+ firstTab.click();
+ }
}
}
}
diff --git a/app/assets/javascripts/pages/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js
index 72d05da1069..758bbafead3 100644
--- a/app/assets/javascripts/pages/snippets/form.js
+++ b/app/assets/javascripts/pages/snippets/form.js
@@ -3,6 +3,13 @@ import GLForm from '~/gl_form';
import ZenMode from '~/zen_mode';
export default () => {
- new GLForm($('.snippet-form'), false); // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new GLForm($('.snippet-form'), {
+ members: false,
+ issues: false,
+ mergeRequests: false,
+ milestones: false,
+ labels: false,
+ });
new ZenMode(); // eslint-disable-line no-new
};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index b49a16a87e6..cf3ff48e608 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -10,7 +10,7 @@ import eventHub from './event_hub';
Vue.use(Translate);
export default () => {
- const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
+ const { dataset } = document.querySelector('.js-pipeline-details-vue');
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 59c8b9c58e5..8317d3f4510 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -19,7 +19,7 @@ export default class PipelinesService {
getPipelines(data = {}) {
const { scope, page } = data;
- const CancelToken = axios.CancelToken;
+ const { CancelToken } = axios;
this.cancelationSource = CancelToken.source();
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 45670584679..0e973cab4d2 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -43,7 +43,7 @@ MarkdownPreview.prototype.showPreview = function ($form) {
this.fetchMarkdownPreview(mdText, url, (function (response) {
var body;
if (response.body.length > 0) {
- body = response.body;
+ ({ body } = response);
} else {
body = this.emptyMessage;
}
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index 40ec3208b58..f641b23e519 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -47,7 +47,8 @@ import _ from 'underscore';
var _this;
_this = this;
this.fileInput.on('change', function(e) {
- return _this.onFileInputChange(e, this);
+ _this.onFileInputChange(e, this);
+ this.value = null;
});
this.pickImageEl.on('click', this.onPickImageClick);
this.modalCrop.on('shown.bs.modal', this.onModalShow);
@@ -85,11 +86,10 @@ import _ from 'underscore';
cropBoxResizable: false,
toggleDragModeOnDblclick: false,
built: function() {
- var $image, container, cropBoxHeight, cropBoxWidth;
- $image = $(this);
- container = $image.cropper('getContainerData');
- cropBoxWidth = _this.cropBoxWidth;
- cropBoxHeight = _this.cropBoxHeight;
+ const $image = $(this);
+ const container = $image.cropper('getContainerData');
+ const { cropBoxWidth, cropBoxHeight } = _this;
+
return $image.cropper('setCropBoxData', {
width: cropBoxWidth,
height: cropBoxHeight,
@@ -136,14 +136,13 @@ import _ from 'underscore';
}
dataURLtoBlob(dataURL) {
- var array, binary, i, k, len, v;
+ var array, binary, i, len, v;
binary = atob(dataURL.split(',')[1]);
array = [];
- // eslint-disable-next-line no-multi-assign
- for (k = i = 0, len = binary.length; i < len; k = (i += 1)) {
- v = binary[k];
- array.push(binary.charCodeAt(k));
+ for (i = 0, len = binary.length; i < len; i += 1) {
+ v = binary[i];
+ array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], {
type: 'image/png'
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index f53c95205af..05485e352dc 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -88,12 +88,11 @@ export default class ProjectFindFile {
// render result
renderList(filePaths, searchText) {
- var blobItemUrl, filePath, html, i, j, len, matches, results;
+ var blobItemUrl, filePath, html, i, len, matches, results;
this.element.find(".tree-table > tbody").empty();
results = [];
- // eslint-disable-next-line no-multi-assign
- for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) {
+ for (i = 0, len = filePaths.length; i < len; i += 1) {
filePath = filePaths[i];
if (i === 20) {
break;
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index c772fca14bb..a4c7c143e56 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -47,7 +47,7 @@
},
methods: {
successCallback(res) {
- const pipelines = res.data.pipelines;
+ const { pipelines } = res.data;
if (pipelines.length > 0) {
// The pipeline entity always keeps the latest pipeline info on the `details.status`
this.ciStatus = pipelines[0].details.status;
diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js
index e1ca70c51a6..6056f12aa4f 100644
--- a/app/assets/javascripts/projects_dropdown/index.js
+++ b/app/assets/javascripts/projects_dropdown/index.js
@@ -31,7 +31,7 @@ document.addEventListener('DOMContentLoaded', () => {
projectsDropdownApp,
},
data() {
- const dataset = this.$options.el.dataset;
+ const { dataset } = this.$options.el;
const store = new ProjectsStore();
const service = new ProjectsService(dataset.userName);
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
index 6fb125192b2..e15cd94a915 100644
--- a/app/assets/javascripts/registry/index.js
+++ b/app/assets/javascripts/registry/index.js
@@ -10,7 +10,7 @@ export default () => new Vue({
registryApp,
},
data() {
- const dataset = document.querySelector(this.$options.el).dataset;
+ const { dataset } = document.querySelector(this.$options.el);
return {
endpoint: dataset.endpoint,
};
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index c0de03373d8..a78aa90b7b5 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -20,7 +20,7 @@ export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => {
- const headers = response.headers;
+ const { headers } = response;
return response.json().then(resp => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js
index 2f974d6ff9d..060f374310c 100644
--- a/app/assets/javascripts/shared/milestones/form.js
+++ b/app/assets/javascripts/shared/milestones/form.js
@@ -6,5 +6,13 @@ import GLForm from '../../gl_form';
export default (initGFM = true) => {
new ZenMode(); // eslint-disable-line no-new
new DueDateSelectors(); // eslint-disable-line no-new
- new GLForm($('.milestone-form'), initGFM); // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new GLForm($('.milestone-form'), {
+ emojis: initGFM,
+ members: initGFM,
+ issues: initGFM,
+ mergeRequests: initGFM,
+ milestones: initGFM,
+ labels: initGFM,
+ });
};
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 193788f754f..e9451be31fd 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -9,12 +9,10 @@ export default class ShortcutsIssuable extends Shortcuts {
constructor(isMergeRequest) {
super();
- this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
-
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
- Mousetrap.bind('r', this.replyWithSelectedText.bind(this));
+ Mousetrap.bind('r', ShortcutsIssuable.replyWithSelectedText);
Mousetrap.bind('e', ShortcutsIssuable.editIssue);
if (isMergeRequest) {
@@ -24,11 +22,16 @@ export default class ShortcutsIssuable extends Shortcuts {
}
}
- replyWithSelectedText() {
+ static replyWithSelectedText() {
+ const $replyField = $('.js-main-target-form .js-vue-comment-form');
const documentFragment = window.gl.utils.getSelectedFragment();
+ if (!$replyField.length) {
+ return false;
+ }
+
if (!documentFragment) {
- this.$replyField.focus();
+ $replyField.focus();
return false;
}
@@ -39,21 +42,22 @@ export default class ShortcutsIssuable extends Shortcuts {
return false;
}
- const quote = _.map(selected.split('\n'), val => `${(`> ${val}`).trim()}\n`);
+ const quote = _.map(selected.split('\n'), val => `${`> ${val}`.trim()}\n`);
// If replyField already has some content, add a newline before our quote
- const separator = (this.$replyField.val().trim() !== '' && '\n\n') || '';
- this.$replyField.val((a, current) => `${current}${separator}${quote.join('')}\n`)
+ const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
+ $replyField
+ .val((a, current) => `${current}${separator}${quote.join('')}\n`)
.trigger('input')
.trigger('change');
// Trigger autosize
const event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
- this.$replyField.get(0).dispatchEvent(event);
+ $replyField.get(0).dispatchEvent(event);
// Focus the input field
- this.$replyField.focus();
+ $replyField.focus();
return false;
}
diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js
index 77ab7c964e6..5e385400747 100644
--- a/app/assets/javascripts/smart_interval.js
+++ b/app/assets/javascripts/smart_interval.js
@@ -42,8 +42,7 @@ export default class SmartInterval {
/* public */
start() {
- const cfg = this.cfg;
- const state = this.state;
+ const { cfg, state } = this;
if (cfg.immediateExecution && !this.isLoading) {
cfg.immediateExecution = false;
@@ -100,7 +99,7 @@ export default class SmartInterval {
/* private */
initInterval() {
- const cfg = this.cfg;
+ const { cfg } = this;
if (!cfg.lazyStart) {
this.start();
@@ -151,7 +150,7 @@ export default class SmartInterval {
}
incrementInterval() {
- const cfg = this.cfg;
+ const { cfg } = this;
const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf;
@@ -166,7 +165,7 @@ export default class SmartInterval {
isPageVisible() { return this.state.pageVisibility === 'visible'; }
stopTimer() {
- const state = this.state;
+ const { state } = this;
state.intervalId = window.clearInterval(state.intervalId);
}
diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
index e39213cb098..a5c18042ce7 100644
--- a/app/assets/javascripts/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -38,14 +38,14 @@ function simulateEvent(el, type, options = {}) {
function isLast(target) {
const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- const children = el.children;
+ const { children } = el;
return children.length - 1 === target.index;
}
function getTarget(target) {
const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- const children = el.children;
+ const { children } = el;
return (
children[target.index] ||
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 96af6d2fcca..78fd7ad441f 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -11,7 +11,6 @@ export default class U2FAuthenticate {
constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
this.u2fUtils = null;
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);
@@ -41,7 +40,6 @@ export default class U2FAuthenticate {
this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge'));
this.templates = {
- notSupported: '#js-authenticate-u2f-not-supported',
setup: '#js-authenticate-u2f-setup',
inProgress: '#js-authenticate-u2f-in-progress',
error: '#js-authenticate-u2f-error',
@@ -55,7 +53,7 @@ export default class U2FAuthenticate {
this.u2fUtils = utils;
this.renderInProgress();
})
- .catch(() => this.renderNotSupported());
+ .catch(() => this.switchToFallbackUI());
}
authenticate() {
@@ -96,10 +94,6 @@ export default class U2FAuthenticate {
this.fallbackButton.classList.add('hidden');
}
- renderNotSupported() {
- return this.renderTemplate('notSupported');
- }
-
switchToFallbackUI() {
this.fallbackButton.classList.add('hidden');
this.container[0].classList.add('hidden');
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 277a7656b8e..e3d7645040d 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -250,7 +250,6 @@ function UsersSelect(currentUser, els, options = {}) {
let anyUser;
let index;
- let j;
let len;
let name;
let obj;
@@ -259,8 +258,7 @@ function UsersSelect(currentUser, els, options = {}) {
showDivider = 0;
if (firstUser) {
// Move current user to the front of the list
- // eslint-disable-next-line no-multi-assign
- for (index = j = 0, len = users.length; j < len; index = (j += 1)) {
+ for (index = 0, len = users.length; index < len; index += 1) {
obj = users[index];
if (obj.username === firstUser) {
users.splice(index, 1);
@@ -502,7 +500,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (this.multiSelect) {
selected = getSelected().find(u => user.id === u);
- const fieldName = this.fieldName;
+ const { fieldName } = this;
const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']");
if (field.length) {
@@ -554,7 +552,7 @@ function UsersSelect(currentUser, els, options = {}) {
minimumInputLength: 0,
query: function(query) {
return _this.users(query.term, options, function(users) {
- var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref;
+ var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
data = {
results: users
};
@@ -563,8 +561,7 @@ function UsersSelect(currentUser, els, options = {}) {
// Move current user to the front of the list
ref = data.results;
- // eslint-disable-next-line no-multi-assign
- for (index = j = 0, len = ref.length; j < len; index = (j += 1)) {
+ for (index = 0, len = ref.length; index < len; index += 1) {
obj = ref[index];
if (obj.username === firstUser) {
data.results.splice(index, 1);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
new file mode 100644
index 00000000000..bf8628d18a6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
@@ -0,0 +1,15 @@
+/*
+The squash-before-merge button is EE only, but it's located right in the middle
+of the readyToMerge state component template.
+
+If we didn't declare this component in CE, we'd need to maintain a separate copy
+of the readyToMergeState template in EE, which is pretty big and likely to change.
+
+Instead, in CE, we declare the component, but it's hidden and is configured to do nothing.
+In EE, the configuration extends this object to add a functioning squash-before-merge
+button.
+*/
+
+export default {
+ template: '',
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 792ee7dc7a2..036a20ff734 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -191,7 +191,7 @@ export default {
if (data.ci_status === this.mr.ciStatus) return;
if (!data.pipeline) return;
- const label = data.pipeline.details.status.label;
+ const { label } = data.pipeline.details.status;
const title = `Pipeline ${label}`;
const message = `Pipeline ${label} for "${data.title}"`;
@@ -211,7 +211,7 @@ export default {
// `params` should be an Array contains a Boolean, like `[true]`
// Passing parameter as Boolean didn't work.
eventHub.$on('SetBranchRemoveFlag', (params) => {
- this.mr.isRemovingSourceBranch = params[0];
+ [this.mr.isRemovingSourceBranch] = params;
});
eventHub.$on('FailedToMerge', (mergeError) => {
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 6851029018a..133bdbb54f7 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -42,7 +42,7 @@ export default {
},
methods: {
onImgLoad() {
- const contentImg = this.$refs.contentImg;
+ const { contentImg } = this.$refs;
if (contentImg) {
this.isZoomable =
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 09e0094054d..a10deb93f0f 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -4,7 +4,7 @@ import { __ } from '~/locale';
import $ from 'jquery';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-const CancelToken = axios.CancelToken;
+const { CancelToken } = axios;
let axiosSource;
export default {
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index 2c47f5b9b35..d3cbe3c7e74 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -45,11 +45,15 @@ export default {
return DownloadDiffViewer;
}
},
+ basePath() {
+ // We might get the project path from rails with the relative url already setup
+ return this.projectPath.indexOf('/') === 0 ? '' : `${gon.relative_url_root}/`;
+ },
fullOldPath() {
- return `${gon.relative_url_root}/${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`;
+ return `${this.basePath}${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`;
},
fullNewPath() {
- return `${gon.relative_url_root}/${this.projectPath}/raw/${this.newSha}/${this.newPath}`;
+ return `${this.basePath}${this.projectPath}/raw/${this.newSha}/${this.newPath}`;
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 05e8ed2da2c..fba67681777 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -62,7 +62,14 @@
/*
GLForm class handles all the toolbar buttons
*/
- return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete);
+ return new GLForm($(this.$refs['gl-form']), {
+ emojis: this.enableAutocomplete,
+ members: this.enableAutocomplete,
+ issues: this.enableAutocomplete,
+ mergeRequests: this.enableAutocomplete,
+ milestones: this.enableAutocomplete,
+ labels: this.enableAutocomplete,
+ });
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('glForm');
@@ -150,7 +157,7 @@
</div>
<div
v-show="previewMarkdown"
- class="md md-preview-holder md-preview"
+ class="md md-preview-holder md-preview js-vue-md-preview"
>
<div
ref="markdown-preview"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index ee3628b1e3f..83171ae50b8 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -71,7 +71,7 @@
class="md-header-tab"
>
<a
- class="js-preview-link"
+ class="js-preview-link js-md-preview-button"
href="#md-preview-holder"
tabindex="-1"
@click.prevent="previewMarkdownTab($event)"
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index 80e3db52cb0..2eb6c20b2c0 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -14,11 +14,12 @@
</template>
<script>
- import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
- export default {
- components: {
- skeletonLoadingContainer,
- },
- };
+export default {
+ name: 'SkeletonNote',
+ components: {
+ skeletonLoadingContainer,
+ },
+};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index aac10f84087..2122d0a508e 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -1,51 +1,75 @@
<script>
- /**
- * Common component to render a system note, icon and user information.
- *
- * This component needs to be used with a vuex store.
- * That vuex store needs to have a `targetNoteHash` getter
- *
- * @example
- * <system-note
- * :note="{
- * id: String,
- * author: Object,
- * createdAt: String,
- * note_html: String,
- * system_note_icon_name: String
- * }"
- * />
- */
- import { mapGetters } from 'vuex';
- import noteHeader from '~/notes/components/note_header.vue';
- import { spriteIcon } from '../../../lib/utils/common_utils';
+/**
+ * Common component to render a system note, icon and user information.
+ *
+ * This component needs to be used with a vuex store.
+ * That vuex store needs to have a `targetNoteHash` getter
+ *
+ * @example
+ * <system-note
+ * :note="{
+ * id: String,
+ * author: Object,
+ * createdAt: String,
+ * note_html: String,
+ * system_note_icon_name: String
+ * }"
+ * />
+ */
+import $ from 'jquery';
+import { mapGetters } from 'vuex';
+import noteHeader from '~/notes/components/note_header.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { spriteIcon } from '../../../lib/utils/common_utils';
- export default {
- name: 'SystemNote',
- components: {
- noteHeader,
+const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
+
+export default {
+ name: 'SystemNote',
+ components: {
+ Icon,
+ noteHeader,
+ },
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ expanded: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['targetNoteHash']),
+ noteAnchorId() {
+ return `note_${this.note.id}`;
+ },
+ isTargetNote() {
+ return this.targetNoteHash === this.noteAnchorId;
},
- props: {
- note: {
- type: Object,
- required: true,
- },
+ iconHtml() {
+ return spriteIcon(this.note.system_note_icon_name);
},
- computed: {
- ...mapGetters([
- 'targetNoteHash',
- ]),
- noteAnchorId() {
- return `note_${this.note.id}`;
- },
- isTargetNote() {
- return this.targetNoteHash === this.noteAnchorId;
- },
- iconHtml() {
- return spriteIcon(this.note.system_note_icon_name);
- },
+ toggleIcon() {
+ return this.expanded ? 'chevron-up' : 'chevron-down';
},
- };
+ // following 2 methods taken from code in `collapseLongCommitList` of notes.js:
+ actionTextHtml() {
+ return $(this.note.note_html)
+ .unwrap()
+ .html();
+ },
+ hasMoreCommits() {
+ return (
+ $(this.note.note_html)
+ .filter('ul')
+ .children().length > MAX_VISIBLE_COMMIT_LIST_COUNT
+ );
+ },
+ },
+};
</script>
<template>
@@ -64,8 +88,35 @@
:author="note.author"
:created-at="note.created_at"
:note-id="note.id"
- :action-text-html="note.note_html"
- />
+ >
+ <span v-html="actionTextHtml"></span>
+ </note-header>
+ </div>
+ <div class="note-body">
+ <div
+ :class="{
+ 'system-note-commit-list': hasMoreCommits,
+ 'hide-shade': expanded
+ }"
+ class="note-text"
+ v-html="note.note_html"
+ ></div>
+ <div
+ v-if="hasMoreCommits"
+ class="flex-list"
+ >
+ <div
+ class="system-note-commit-list-toggler flex-row"
+ @click="expanded = !expanded"
+ >
+ <Icon
+ :name="toggleIcon"
+ :size="8"
+ class="append-right-5"
+ />
+ <span>Toggle commit list</span>
+ </div>
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue
index 2370e59d017..8e9621c956f 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue
@@ -55,7 +55,7 @@
},
getItems() {
const total = this.pageInfo.totalPages;
- const page = this.pageInfo.page;
+ const { page } = this.pageInfo;
const items = [];
if (page > 1) {
diff --git a/app/assets/stylesheets/bootstrap.scss b/app/assets/stylesheets/bootstrap.scss
new file mode 100644
index 00000000000..a040c2f8c20
--- /dev/null
+++ b/app/assets/stylesheets/bootstrap.scss
@@ -0,0 +1,37 @@
+/*
+ * Includes specific styles from the bootstrap4 foler in node_modules
+ */
+
+@import "../../../node_modules/bootstrap/scss/functions";
+@import "../../../node_modules/bootstrap/scss/variables";
+@import "../../../node_modules/bootstrap/scss/mixins";
+@import "../../../node_modules/bootstrap/scss/root";
+@import "../../../node_modules/bootstrap/scss/reboot";
+@import "../../../node_modules/bootstrap/scss/type";
+@import "../../../node_modules/bootstrap/scss/images";
+@import "../../../node_modules/bootstrap/scss/code";
+@import "../../../node_modules/bootstrap/scss/grid";
+@import "../../../node_modules/bootstrap/scss/tables";
+@import "../../../node_modules/bootstrap/scss/forms";
+@import "../../../node_modules/bootstrap/scss/buttons";
+@import "../../../node_modules/bootstrap/scss/transitions";
+@import "../../../node_modules/bootstrap/scss/dropdown";
+@import "../../../node_modules/bootstrap/scss/button-group";
+@import "../../../node_modules/bootstrap/scss/input-group";
+@import "../../../node_modules/bootstrap/scss/custom-forms";
+@import "../../../node_modules/bootstrap/scss/nav";
+@import "../../../node_modules/bootstrap/scss/navbar";
+@import "../../../node_modules/bootstrap/scss/card";
+@import "../../../node_modules/bootstrap/scss/breadcrumb";
+@import "../../../node_modules/bootstrap/scss/pagination";
+@import "../../../node_modules/bootstrap/scss/badge";
+@import "../../../node_modules/bootstrap/scss/alert";
+@import "../../../node_modules/bootstrap/scss/progress";
+@import "../../../node_modules/bootstrap/scss/media";
+@import "../../../node_modules/bootstrap/scss/list-group";
+@import "../../../node_modules/bootstrap/scss/close";
+@import "../../../node_modules/bootstrap/scss/modal";
+@import "../../../node_modules/bootstrap/scss/tooltip";
+@import "../../../node_modules/bootstrap/scss/popover";
+@import "../../../node_modules/bootstrap/scss/utilities";
+@import "../../../node_modules/bootstrap/scss/print";
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 0d8e867f41d..f610a1aea08 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -173,6 +173,12 @@ table {
display: none;
}
+h3.popover-header {
+ // Default bootstrap popovers use <h3>
+ // which we default to having a top margin
+ margin-top: 0;
+}
+
// Add to .label so that old system notes that are saved to the db
// will still receive the correct styling
.badge,
@@ -304,7 +310,7 @@ pre code {
color: $white-light;
h4,
- a,
+ a:not(.btn),
.alert-link {
color: $white-light;
}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 7c28024001f..c46b0b5db09 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -1,6 +1,7 @@
@import 'framework/variables';
@import 'framework/mixins';
-@import '../../../node_modules/bootstrap/scss/bootstrap';
+
+@import 'bootstrap';
@import 'bootstrap_migration';
@import 'framework/layout';
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 14cd32da9eb..549a8730301 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -251,3 +251,12 @@ $skeleton-line-widths: (
transform: translateX(468px);
}
}
+
+.slide-down-enter-active {
+ transition: transform 0.2s;
+}
+
+.slide-down-enter,
+.slide-down-leave-to {
+ transform: translateY(-30%);
+}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 0de05548c68..1d4828be223 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -13,6 +13,7 @@
&.diff-collapsed {
padding: 5px;
+ line-height: 34px;
.click-to-expand {
cursor: pointer;
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index b40d02f381a..aaa8bed3df0 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -180,10 +180,6 @@
color: $border-and-box-shadow;
}
- .ide-file-list .file.file-active {
- color: $border-and-box-shadow;
- }
-
.ide-sidebar-link {
&.active {
color: $border-and-box-shadow;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 2fa71b23314..5789c3fa1b1 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -527,7 +527,7 @@
.header-user {
.dropdown-menu {
width: auto;
- min-width: 160px;
+ min-width: unset;
margin-top: 4px;
color: $gl-text-color;
left: auto;
@@ -539,6 +539,10 @@
display: block;
}
}
+
+ svg {
+ vertical-align: text-top;
+ }
}
}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index f0ac9b46f91..604f806dc58 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -111,7 +111,9 @@ $dark-il: #de935f;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index eba7919ada9..8e2720511da 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -111,7 +111,9 @@ $monokai-gi: #a6e22e;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index ba53ef0352b..cd1f0f6650f 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -115,7 +115,9 @@ $solarized-dark-il: #2aa198;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index e9fccf1b58a..09c3ea36414 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -122,7 +122,9 @@ $solarized-light-il: #2aa198;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include matchLine;
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 7c1d1626f1c..750d2c8b990 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -290,10 +290,6 @@
&.is-active,
&.is-active .board-card-assignee:hover a {
background-color: $row-hover;
-
- &:first-child:not(:only-child) {
- box-shadow: -10px 0 10px 1px $row-hover;
- }
}
.badge {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index fbc97ec0c95..7b8e66b6f12 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -24,6 +24,10 @@
color: $gl-text-color;
border-radius: 0 0 3px 3px;
+ .code {
+ padding: 0;
+ }
+
.unfold {
cursor: pointer;
}
@@ -61,6 +65,7 @@
.diff-line-num {
width: 50px;
+ position: relative;
a {
transition: none;
@@ -77,6 +82,12 @@
span {
white-space: pre-wrap;
+
+ &.context-cell {
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ }
}
.line {
@@ -491,6 +502,10 @@
border-bottom: 0;
}
+.merge-request-details .file-content.image_file img {
+ max-height: 50vh;
+}
+
.diff-stats-summary-toggler {
padding: 0;
background-color: transparent;
@@ -677,21 +692,21 @@
}
@include media-breakpoint-up(sm) {
- position: -webkit-sticky;
- position: sticky;
top: 24px;
background-color: $white-light;
- z-index: 190;
&.diff-files-changed-merge-request {
- top: 76px;
+ position: sticky;
+ top: 90px;
+ z-index: 190;
+ margin: $gl-padding 0;
+ padding: 0;
}
&.is-stuck {
padding-top: 0;
padding-bottom: 0;
border-bottom: 1px solid $white-dark;
- transform: translateY(16px);
.diff-stats-additions-deletions-expanded,
.inline-parallel-buttons {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index b0fabaec46b..9a18fdd2bb1 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -81,7 +81,6 @@
.btn {
font-size: $gl-font-size;
- height: 32px;
&[disabled] {
opacity: 0.3;
@@ -650,14 +649,12 @@
position: relative;
background: $gray-light;
color: $gl-text-color;
- z-index: 199;
.mr-version-menus-container {
- display: -webkit-flex;
display: flex;
- -webkit-align-items: center;
align-items: center;
padding: 16px;
+ z-index: 199;
}
.content-block {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 3849a04db5d..5e5696b1602 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -247,22 +247,6 @@
}
.discussion-with-resolve-btn {
- display: table;
- width: 100%;
- border-collapse: separate;
- table-layout: auto;
-
- .btn-group {
- display: table-cell;
- float: none;
- width: 1%;
-
- &:first-child {
- width: 100%;
- padding-right: 5px;
- }
- }
-
.discussion-actions {
display: table;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 299eda53140..25400d886fb 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -3,9 +3,17 @@
*/
@-webkit-keyframes targe3-note {
- from { background: $note-targe3-outside; }
- 50% { background: $note-targe3-inside; }
- to { background: $note-targe3-outside; }
+ from {
+ background: $note-targe3-outside;
+ }
+
+ 50% {
+ background: $note-targe3-inside;
+ }
+
+ to {
+ background: $note-targe3-outside;
+ }
}
ul.notes {
@@ -33,10 +41,12 @@ ul.notes {
.diff-content {
overflow: visible;
+ padding: 0;
}
}
- > li { // .timeline-entry
+ > li {
+ // .timeline-entry
padding: 0;
display: block;
position: relative;
@@ -153,7 +163,6 @@ ul.notes {
}
.note-header {
-
@include notes-media('max', map-get($grid-breakpoints, xs)) {
.inline {
display: block;
@@ -245,7 +254,6 @@ ul.notes {
.system-note-commit-list-toggler {
color: $gl-link-color;
- display: none;
padding: 10px 0 0;
cursor: pointer;
position: relative;
@@ -624,20 +632,18 @@ ul.notes {
.line_holder .is-over:not(.no-comment-btn) {
.add-diff-note {
opacity: 1;
+ z-index: 101;
}
}
.add-diff-note {
@include btn-comment-icon;
opacity: 0;
- margin-top: -2px;
margin-left: -55px;
position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
z-index: 10;
-
- .new & {
- margin-top: -10px;
- }
}
.discussion-body,
@@ -665,7 +671,6 @@ ul.notes {
background-color: $white-light;
}
-
a {
color: $gl-link-color;
}
@@ -771,3 +776,44 @@ ul.notes {
height: auto;
}
}
+
+// Vue refactored diff discussion adjustments
+.files {
+ .diff-discussions {
+ .note-discussion.timeline-entry {
+ padding-left: 0;
+
+ &:last-child {
+ border-bottom: 0;
+ }
+
+ > .timeline-entry-inner {
+ padding: 0;
+
+ > .timeline-content {
+ margin-left: 0;
+ }
+
+ > .timeline-icon {
+ display: none;
+ }
+ }
+
+ .discussion-body {
+ padding-top: 0;
+
+ .discussion-wrapper {
+ border-color: transparent;
+ }
+ }
+ }
+ }
+
+ .diff-comment-form {
+ display: block;
+ }
+
+ .add-diff-note svg {
+ margin-top: 4px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 0a56153203c..3c24aaa65e8 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -23,6 +23,7 @@
margin-top: 0;
border-top: 1px solid $white-dark;
padding-bottom: $ide-statusbar-height;
+ color: $gl-text-color;
&.is-collapsed {
.ide-file-list {
@@ -45,12 +46,8 @@
.file {
cursor: pointer;
- &.file-open {
- background: $white-normal;
- }
-
&.file-active {
- font-weight: $gl-font-weight-bold;
+ background: $theme-gray-100;
}
.ide-file-name {
@@ -58,7 +55,9 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: inherit;
- line-height: 22px;
+ line-height: 16px;
+ display: inline-block;
+ height: 18px;
svg {
vertical-align: middle;
@@ -86,12 +85,14 @@
.ide-new-btn {
display: none;
+
+ .btn {
+ padding: 2px 5px;
+ }
}
&:hover,
&:focus {
- background: $white-normal;
-
.ide-new-btn {
display: block;
}
@@ -281,8 +282,8 @@
}
.margin {
- background-color: $gray-light;
- border-right: 1px solid $white-normal;
+ background-color: $white-light;
+ border-right: 1px solid $theme-gray-100;
.line-insert {
border-right: 1px solid $line-added-dark;
@@ -303,6 +304,15 @@
.multi-file-editor-holder {
height: 100%;
min-height: 0;
+
+ &.is-readonly,
+ .editor.original {
+ .monaco-editor,
+ .monaco-editor-background,
+ .monaco-editor .inputarea.ime-input {
+ background-color: $theme-gray-50;
+ }
+ }
}
.preview-container {
@@ -587,11 +597,17 @@
&:hover,
&:focus {
- background: $white-normal;
+ background: $theme-gray-100;
+ }
+
+ &:active {
+ background: $theme-gray-200;
}
}
.multi-file-commit-list-path {
+ cursor: pointer;
+
&.is-active {
background-color: $white-normal;
}
@@ -611,10 +627,6 @@
.multi-file-commit-list-file-path {
@include str-truncated(calc(100% - 30px));
- &:hover {
- text-decoration: underline;
- }
-
&:active {
text-decoration: none;
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 765c926751a..2d66f336076 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -114,7 +114,7 @@ input[type="checkbox"]:hover {
}
.dropdown-content {
- max-height: 302px;
+ max-height: none;
}
}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index cdfe3d6ab1e..9723e400574 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -52,7 +52,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
private
def set_application_setting
- @application_setting = ApplicationSetting.current_without_cache
+ @application_setting = Gitlab::CurrentSettings.current_application_settings
end
def application_setting_params
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 001f6520093..96b7bc65ac9 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -72,10 +72,10 @@ class Admin::GroupsController < Admin::ApplicationController
end
def group_params
- params.require(:group).permit(group_params_ce)
+ params.require(:group).permit(allowed_group_params)
end
- def group_params_ce
+ def allowed_group_params
[
:avatar,
:description,
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index bfeb5a2d097..653f3dfffc4 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -187,10 +187,10 @@ class Admin::UsersController < Admin::ApplicationController
end
def user_params
- params.require(:user).permit(user_params_ce)
+ params.require(:user).permit(allowed_user_params)
end
- def user_params_ce
+ def allowed_user_params
[
:access_level,
:avatar,
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 56312f801fb..21cc6dfdd16 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -27,7 +27,7 @@ class ApplicationController < ActionController::Base
after_action :set_page_title_header, if: -> { request.format == :json }
- protect_from_forgery with: :exception
+ protect_from_forgery with: :exception, prepend: true
helper_method :can?
helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index d04eb192129..ba510968684 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -90,7 +90,7 @@ module IssuableActions
end
def discussions
- notes = issuable.notes
+ notes = issuable.discussion_notes
.inc_relations_for_view
.includes(:noteable)
.fresh
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 0c34e49206a..fe9a030cdf2 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -237,10 +237,6 @@ module NotesActions
def use_note_serializer?
return false if params['html']
- if noteable.is_a?(MergeRequest)
- cookies[:vue_mr_discussions] == 'true'
- else
- noteable.discussions_rendered_on_frontend?
- end
+ noteable.discussions_rendered_on_frontend?
end
end
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index 16abf7bab7e..e54f372344d 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -1,5 +1,5 @@
class HealthController < ActionController::Base
- protect_from_forgery with: :exception, except: :storage_check
+ protect_from_forgery with: :exception, except: :storage_check, prepend: true
include RequiresWhitelistedMonitoringClient
CHECKS = [
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index 33b682d2859..0400ffcfee5 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -1,7 +1,7 @@
class MetricsController < ActionController::Base
include RequiresWhitelistedMonitoringClient
- protect_from_forgery with: :exception
+ protect_from_forgery with: :exception, prepend: true
def index
response = if Gitlab::Metrics.prometheus_metrics_enabled?
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 27fd5f7ba37..1547d4b5972 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -2,7 +2,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
- protect_from_forgery except: [:kerberos, :saml, :cas3]
+ protect_from_forgery except: [:kerberos, :saml, :cas3], prepend: true
def handle_omniauth
omniauth_flow(Gitlab::Auth::OAuth)
@@ -119,7 +119,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
set_remember_me(user)
- if user.two_factor_enabled?
+ if user.two_factor_enabled? && !auth_user.bypass_two_factor?
prompt_for_two_factor(user)
else
sign_in_and_redirect(user)
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index a8c0a68fc17..ebc61264b39 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -3,8 +3,8 @@ class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
include CreatesCommit
include RendersBlob
+ include NotesHelper
include ActionView::Helpers::SanitizeHelper
-
prepend_before_action :authenticate_user!, only: [:edit]
before_action :set_request_format, only: [:edit, :show, :update]
@@ -93,6 +93,7 @@ class Projects::BlobController < Projects::ApplicationController
@lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines
@form = UnfoldForm.new(params)
+
@lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe)
if @form.bottom?
@@ -103,11 +104,50 @@ class Projects::BlobController < Projects::ApplicationController
@match_line = "@@ -#{line}+#{line} @@"
end
- render layout: false
+ # We can keep only 'render_diff_lines' from this conditional when
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/44988 is done
+ if rendered_for_merge_request?
+ render_diff_lines
+ else
+ render layout: false
+ end
end
private
+ # Converts a String array to Gitlab::Diff::Line array
+ def render_diff_lines
+ @lines.map! do |line|
+ # These are marked as context lines but are loaded from blobs.
+ # We also have context lines loaded from diffs in other places.
+ diff_line = Gitlab::Diff::Line.new(line, 'context', nil, nil, nil)
+ diff_line.rich_text = line
+ diff_line
+ end
+
+ add_match_line
+
+ render json: @lines
+ end
+
+ def add_match_line
+ return unless @form.unfold?
+
+ if @form.bottom? && @form.to < @blob.lines.size
+ old_pos = @form.to - @form.offset
+ new_pos = @form.to
+ elsif @form.since != 1
+ old_pos = new_pos = @form.since
+ end
+
+ # Match line is not needed when it reaches the top limit or bottom limit of the file.
+ return unless new_pos
+
+ @match_line = Gitlab::Diff::Line.new(@match_line, 'match', nil, old_pos, new_pos)
+
+ @form.bottom? ? @lines.push(@match_line) : @lines.unshift(@match_line)
+ end
+
def blob
@blob ||= @repository.blob_at(@commit.id, @path)
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 7b7cb52d7ed..9e495061f4e 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -9,6 +9,7 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :assign_ref_vars
before_action :authorize_download_code!
before_action :set_commits
+ before_action :set_request_format, only: :show
def show
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
@@ -61,6 +62,19 @@ class Projects::CommitsController < Projects::ApplicationController
@commits = prepare_commits_for_rendering(@commits)
end
+ # Rails 5 sets request.format from the extension.
+ # Explicitly set to :html.
+ def set_request_format
+ request.format = :html if set_request_format?
+ end
+
+ # Rails 5 sets request.format from extension.
+ # In this case if the ref ends with `.atom`, it's expected to be the html response,
+ # not the atom one. So explicitly set request.format as :html to act like rails4.
+ def set_request_format?
+ request.format.to_s == "text/html" || @commits.ref.ends_with?("atom")
+ end
+
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42330')
end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 8e86af43fee..78b9d53a780 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -21,7 +21,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
def show
render json: {
- discussion_html: view_to_html_string('discussions/_diff_with_notes', discussion: discussion, expanded: true)
+ truncated_diff_lines: discussion.try(:truncated_diff_lines)
}
end
@@ -29,11 +29,6 @@ class Projects::DiscussionsController < Projects::ApplicationController
def render_discussion
if serialize_notes?
- # TODO - It is not needed to serialize notes when resolving
- # or unresolving discussions. We should remove this behavior
- # passing a parameter to DiscussionEntity to return an empty array
- # for notes.
- # Check issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/42853
prepare_notes_for_rendering(discussion.notes, merge_request)
render_json_with_discussions_serializer
else
@@ -44,7 +39,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
def render_json_with_discussions_serializer
render json:
DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity: ProjectNoteEntity)
- .represent(discussion, context: self)
+ .represent(discussion, context: self, render_truncated_diff_lines: true)
end
# Legacy method used to render discussions notes when not using Vue on views.
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index dd12d30a085..63f0aea3195 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -160,7 +160,7 @@ class Projects::JobsController < Projects::ApplicationController
def build
@build ||= project.builds.find(params[:id])
- .present(current_user: current_user)
+ .present(current_user: current_user)
end
def build_path(build)
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index fe8525a488c..48e02581d54 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -9,17 +9,21 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
before_action :define_diff_comment_vars
def show
- @environment = @merge_request.environments_for(current_user).last
-
- render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
+ render_diffs
end
def diff_for_path
- render_diff_for_path(@diffs)
+ render_diffs
end
private
+ def render_diffs
+ @environment = @merge_request.environments_for(current_user).last
+
+ render json: DiffsSerializer.new(current_user: current_user).represent(@diffs, additional_attributes)
+ end
+
def define_diff_vars
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
@compare = commit || find_merge_request_diff_compare
@@ -63,6 +67,19 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end
end
+ def additional_attributes
+ {
+ environment: @environment,
+ merge_request: @merge_request,
+ merge_request_diff: @merge_request_diff,
+ merge_request_diffs: @merge_request_diffs,
+ start_version: @start_version,
+ start_sha: @start_sha,
+ commit: @commit,
+ latest_diff: @merge_request_diff&.latest?
+ }
+ end
+
def define_diff_comment_vars
@new_diff_note_attrs = {
noteable_type: 'MergeRequest',
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 38918b3cd52..a7c5f858c42 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -42,6 +42,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@noteable = @merge_request
@commits_count = @merge_request.commits_count
+ # TODO cleanup- Fatih Simon Create an issue to remove these after the refactoring
+ # we no longer render notes here. I see it will require a small frontend refactoring,
+ # since we gather some data from this collection.
@discussions = @merge_request.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 242e6491456..aa844e94d89 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -95,6 +95,7 @@ class Projects::WikisController < Projects::ApplicationController
def destroy
@page = @project_wiki.find_page(params[:id])
+
WikiPages::DestroyService.new(@project, current_user).execute(@page)
redirect_to project_wiki_path(@project, :home),
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index efb30ba4715..c2492a137fb 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -63,7 +63,7 @@ class ProjectsController < Projects::ApplicationController
redirect_to(edit_project_path(@project))
end
else
- flash[:alert] = result[:message]
+ flash.now[:alert] = result[:message]
format.html { render 'edit' }
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1a339f76d26..7aa277b3614 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -3,21 +3,27 @@ class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper
+ include Recaptcha::Verify
skip_before_action :check_two_factor_requirement, only: [:destroy]
prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create]
+ prepend_before_action :check_captcha, only: [:create]
prepend_before_action :store_redirect_uri, only: [:new]
+ prepend_before_action :ldap_servers, only: [:new, :create]
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
after_action :log_failed_login, only: [:new], if: :failed_login?
+ helper_method :captcha_enabled?
+
+ CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'.freeze
+
def new
set_minimum_password_length
- @ldap_servers = Gitlab::Auth::LDAP::Config.available_servers
super
end
@@ -46,6 +52,25 @@ class SessionsController < Devise::SessionsController
private
+ def captcha_enabled?
+ request.headers[CAPTCHA_HEADER] && Gitlab::Recaptcha.enabled?
+ end
+
+ # From https://github.com/plataformatec/devise/wiki/How-To:-Use-Recaptcha-with-Devise#devisepasswordscontroller
+ def check_captcha
+ return unless user_params[:password].present?
+ return unless captcha_enabled?
+ return unless Gitlab::Recaptcha.load_configurations!
+
+ unless verify_recaptcha
+ self.resource = resource_class.new
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ flash.delete :recaptcha_error
+
+ respond_with_navigational(resource) { render :new }
+ end
+ end
+
def log_failed_login
Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}")
end
@@ -152,6 +177,10 @@ class SessionsController < Devise::SessionsController
Gitlab::Recaptcha.load_configurations!
end
+ def ldap_servers
+ @ldap_servers ||= Gitlab::Auth::LDAP::Config.available_servers
+ end
+
def authentication_method
if user_params[:otp_attempt]
"two-factor"
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 35f4ff2f62f..9b7a35fb3b5 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -83,7 +83,7 @@ class NotesFinder
when "personal_snippet"
PersonalSnippet.all
else
- raise 'invalid target_type'
+ raise "invalid target_type '#{noteable_type}'"
end
end
diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb
index 65d6e019746..74776b2ed1f 100644
--- a/app/finders/user_recent_events_finder.rb
+++ b/app/finders/user_recent_events_finder.rb
@@ -56,7 +56,7 @@ class UserRecentEventsFinder
visible = target_user
.project_interactions
- .where(visibility_level: [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC])
+ .where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(current_user))
.select(:id)
Gitlab::SQL::Union.new([authorized, visible]).to_sql
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 5ff06b3e0fc..82a7931c557 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -86,6 +86,8 @@ module MergeRequestsHelper
end
def version_index(merge_request_diff)
+ return nil if @merge_request_diffs.empty?
+
@merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 7f67574a428..e1a0cf1604c 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -143,7 +143,14 @@ module NotesHelper
notesIds: @notes.map(&:id),
now: Time.now.to_i,
diffView: diff_view,
- autocomplete: autocomplete
+ enableGFM: {
+ emojis: true,
+ members: autocomplete,
+ issues: autocomplete,
+ mergeRequests: autocomplete,
+ milestones: autocomplete,
+ labels: autocomplete
+ }
}
end
@@ -174,11 +181,11 @@ module NotesHelper
discussion.resolved_by_push? ? 'Automatically resolved' : 'Resolved'
end
- def has_vue_discussions_cookie?
- cookies[:vue_mr_discussions] == 'true'
+ def rendered_for_merge_request?
+ params[:from_merge_request].present?
end
def serialize_notes?
- has_vue_discussions_cookie? && !params['html']
+ rendered_for_merge_request? || params['html'].nil?
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index be3958c40a4..c7a434ea092 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -40,7 +40,8 @@ module ProjectsHelper
name_tag_options[:class] << 'has-tooltip'
end
- content_tag(:span, sanitize(username), name_tag_options)
+ # NOTE: ActionView::Helpers::TagHelper#content_tag HTML escapes username
+ content_tag(:span, username, name_tag_options)
end
def link_to_member(project, author, opts = {}, &block)
@@ -506,6 +507,14 @@ module ProjectsHelper
end
end
+ def sidebar_projects_paths
+ %w[
+ projects#show
+ projects#activity
+ cycle_analytics#show
+ ]
+ end
+
def sidebar_settings_paths
%w[
projects#edit
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 5ba3a4a322c..70509e9066d 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -59,8 +59,6 @@ module Emails
def merge_request_unmergeable_email(recipient_id, merge_request_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
- @reasons = MergeRequestPresenter.new(@merge_request, current_user: current_user).unmergeable_reasons
-
mail_answer_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id, reason))
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 3d58a14882f..bddeb8b0352 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -212,14 +212,6 @@ class ApplicationSetting < ActiveRecord::Base
end
end
- validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
- value&.each do |source|
- unless Devise.omniauth_providers.include?(source.to_sym)
- record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
- end
- end
- end
-
validate :terms_exist, if: :enforce_terms?
before_validation :ensure_uuid!
@@ -330,6 +322,11 @@ class ApplicationSetting < ActiveRecord::Base
::Gitlab::Database.cached_column_exists?(:application_settings, :sidekiq_throttling_enabled)
end
+ def disabled_oauth_sign_in_sources=(sources)
+ sources = (sources || []).map(&:to_s) & Devise.omniauth_providers.map(&:to_s)
+ super(sources)
+ end
+
def domain_whitelist_raw
self.domain_whitelist&.join("\n")
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index f430f18ca9a..e5caa3ffa41 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -561,9 +561,9 @@ module Ci
.append(key: 'CI_PIPELINE_IID', value: iid.to_s)
.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
- .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message)
- .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title)
- .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description)
+ .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
+ .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
+ .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
end
def queued_duration
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 44150b37708..b93c1145f82 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -107,6 +107,10 @@ module Issuable
false
end
+ def etag_caching_enabled?
+ false
+ end
+
def has_multiple_assignees?
assignees.count > 1
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index db7254c27e0..cb76ae971d4 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -12,8 +12,8 @@ module Sortable
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc) }
- scope :order_name_asc, -> { reorder("lower(name) asc") }
- scope :order_name_desc, -> { reorder("lower(name) desc") }
+ scope :order_name_asc, -> { reorder(Arel::Nodes::Ascending.new(arel_table[:name].lower)) }
+ scope :order_name_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:name].lower)) }
end
module ClassMethods
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 92482a1a875..35a0ef00856 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -17,6 +17,10 @@ class Discussion
to: :first_note
+ def project_id
+ project&.id
+ end
+
def self.build(notes, context_noteable = nil)
notes.first.discussion_class(context_noteable).new(notes, context_noteable)
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d136700836d..d3df2da14e2 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -308,6 +308,10 @@ class Issue < ActiveRecord::Base
end
end
+ def etag_caching_enabled?
+ true
+ end
+
def discussions_rendered_on_frontend?
true
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 1cf04976602..7bbcaa121ca 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -85,11 +85,16 @@ class Label < ActiveRecord::Base
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
- (?<label_id>\d+(?!\S\w)\b) | # Integer-based label ID, or
- (?<label_name>
- [A-Za-z0-9_\-\?\.&]+ | # String-based single-word label title, or
- ".+?" # String-based multi-word label surrounded in quotes
- )
+ (?<label_id>\d+(?!\S\w)\b)
+ | # Integer-based label ID, or
+ (?<label_name>
+ # String-based single-word label title, or
+ [A-Za-z0-9_\-\?\.&]+
+ (?<!\.|\?)
+ |
+ # String-based multi-word label surrounded in quotes
+ ".+?"
+ )
)
}x
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 324065c1162..6c96c8ca391 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -128,8 +128,15 @@ class MergeRequest < ActiveRecord::Base
end
after_transition unchecked: :cannot_be_merged do |merge_request, transition|
- NotificationService.new.merge_request_unmergeable(merge_request)
- TodoService.new.merge_request_became_unmergeable(merge_request)
+ begin
+ if merge_request.notify_conflict?
+ NotificationService.new.merge_request_unmergeable(merge_request)
+ TodoService.new.merge_request_became_unmergeable(merge_request)
+ end
+ rescue Gitlab::Git::CommandError
+ # Checking mergeability can trigger exception, e.g. non-utf8
+ # We ignore this type of errors.
+ end
end
def check_state?(merge_status)
@@ -369,6 +376,10 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def non_latest_diffs
+ merge_request_diffs.where.not(id: merge_request_diff.id)
+ end
+
def diff_size
# Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here.
@@ -610,18 +621,7 @@ class MergeRequest < ActiveRecord::Base
def reload_diff(current_user = nil)
return unless open?
- old_diff_refs = self.diff_refs
- new_diff = create_merge_request_diff
-
- MergeRequests::MergeRequestDiffCacheService.new.execute(self, new_diff)
-
- new_diff_refs = self.diff_refs
-
- update_diff_discussion_positions(
- old_diff_refs: old_diff_refs,
- new_diff_refs: new_diff_refs,
- current_user: current_user
- )
+ MergeRequests::ReloadDiffsService.new(self, current_user).execute
end
def check_if_can_be_merged
@@ -706,6 +706,10 @@ class MergeRequest < ActiveRecord::Base
should_remove_source_branch? || force_remove_source_branch?
end
+ def notify_conflict?
+ (opened? || locked?) && !project.repository.can_be_merged?(diff_head_sha, target_branch)
+ end
+
def related_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
@@ -1115,6 +1119,10 @@ class MergeRequest < ActiveRecord::Base
true
end
+ def discussions_rendered_on_frontend?
+ true
+ end
+
def update_project_counter_caches
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 06aa67c600f..3d72c447b4b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -3,6 +3,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Importable
include ManualInverseAssociation
include IgnorableColumn
+ include EachBatch
# Don't display more than 100 commits at once
COMMITS_SAFE_SIZE = 100
@@ -17,8 +18,14 @@ class MergeRequestDiff < ActiveRecord::Base
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
state_machine :state, initial: :empty do
+ event :clean do
+ transition any => :without_files
+ end
+
state :collected
state :overflow
+ # Diff files have been deleted by the system
+ state :without_files
# Deprecated states: these are no longer used but these values may still occur
# in the database.
state :timeout
@@ -27,6 +34,7 @@ class MergeRequestDiff < ActiveRecord::Base
state :overflow_diff_lines_limit
end
+ scope :with_files, -> { without_states(:without_files, :empty) }
scope :viewable, -> { without_state(:empty) }
scope :by_commit_sha, ->(sha) do
joins(:merge_request_diff_commits).where(merge_request_diff_commits: { sha: sha }).reorder(nil)
@@ -42,6 +50,10 @@ class MergeRequestDiff < ActiveRecord::Base
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
end
+ def viewable?
+ collected? || without_files? || overflow?
+ end
+
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
@@ -170,6 +182,21 @@ class MergeRequestDiff < ActiveRecord::Base
end
def diffs(diff_options = nil)
+ if without_files? && comparison = diff_refs.compare_in(project)
+ # It should fetch the repository when diffs are cleaned by the system.
+ # We don't keep these for storage overload purposes.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/37639
+ comparison.diffs(diff_options)
+ else
+ diffs_collection(diff_options)
+ end
+ end
+
+ # Should always return the DB persisted diffs collection
+ # (e.g. Gitlab::Diff::FileCollection::MergeRequestDiff.
+ # It's useful when trying to invalidate old caches through
+ # FileCollection::MergeRequestDiff#clear_cache!
+ def diffs_collection(diff_options = nil)
Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options)
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 52fe529c016..7034c633268 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -228,6 +228,10 @@ class Namespace < ActiveRecord::Base
parent.present?
end
+ def root_ancestor
+ ancestors.reorder(nil).find_by(parent_id: nil)
+ end
+
def subgroup?
has_parent?
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 41c04ae0571..abc40d9016e 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -384,6 +384,7 @@ class Note < ActiveRecord::Base
def expire_etag_cache
return unless noteable&.discussions_rendered_on_frontend?
+ return unless noteable&.etag_caching_enabled?
Gitlab::EtagCaching::Store.new.touch(etag_key)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0d777515536..d91d7dcfe9a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2019,6 +2019,10 @@ class Project < ActiveRecord::Base
end
request_cache(:any_lfs_file_locks?) { self.id }
+ def auto_cancel_pending_pipelines?
+ auto_cancel_pending_pipelines == 'enabled'
+ end
+
private
def storage
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index d7d6aaceb27..faa831b1949 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -29,8 +29,8 @@ class ProjectAutoDevops < ActiveRecord::Base
end
if manual?
- variables.append(key: 'STAGING_ENABLED', value: 1)
- variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 1)
+ variables.append(key: 'STAGING_ENABLED', value: '1')
+ variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1')
end
end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 33280eda0b9..9a38806baab 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -24,7 +24,7 @@ class ProjectTeam
end
def add_role(user, role, current_user: nil)
- send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
+ public_send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
end
def find_member(user_id)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 3089d0162ee..3056c20516a 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -21,7 +21,7 @@ class Repository
attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
- delegate :bundle_to_disk, :create_from_bundle, to: :raw_repository
+ delegate :bundle_to_disk, to: :raw_repository
CreateTreeError = Class.new(StandardError)
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 8d466c33510..eb54ab2cda6 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -20,17 +20,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
end
- def unmergeable_reasons
- strong_memoize(:unmergeable_reasons) do
- reasons = []
- reasons << "no commits" if merge_request.has_no_commits?
- reasons << "source branch is missing" unless merge_request.source_branch_exists?
- reasons << "target branch is missing" unless merge_request.target_branch_exists?
- reasons << "has merge conflicts" unless merge_request.project.repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch)
- reasons
- end
- end
-
def cancel_merge_when_pipeline_succeeds_path
if can_cancel_merge_when_pipeline_succeeds?(current_user)
cancel_merge_when_pipeline_succeeds_project_merge_request_path(project, merge_request)
diff --git a/app/serializers/blob_entity.rb b/app/serializers/blob_entity.rb
index ad039a2623d..b501fd5e964 100644
--- a/app/serializers/blob_entity.rb
+++ b/app/serializers/blob_entity.rb
@@ -3,11 +3,13 @@ class BlobEntity < Grape::Entity
expose :id, :path, :name, :mode
+ expose :readable_text?, as: :readable_text
+
expose :icon do |blob|
IconsHelper.file_type_icon_class('file', blob.mode, blob.name)
end
- expose :url do |blob|
+ expose :url, if: -> (*) { request.respond_to?(:ref) } do |blob|
project_blob_path(request.project, File.join(request.ref, blob.path))
end
end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index 6e68d275047..aa289a96975 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -1,25 +1,46 @@
class DiffFileEntity < Grape::Entity
+ include RequestAwareEntity
+ include BlobHelper
+ include CommitsHelper
include DiffHelper
include SubmoduleHelper
include BlobHelper
include IconsHelper
- include ActionView::Helpers::TagHelper
+ include TreeHelper
+ include ChecksCollaboration
+ include Gitlab::Utils::StrongMemoize
expose :submodule?, as: :submodule
expose :submodule_link do |diff_file|
- submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository).first
+ memoized_submodule_links(diff_file).first
+ end
+
+ expose :submodule_tree_url do |diff_file|
+ memoized_submodule_links(diff_file).last
end
- expose :blob_path do |diff_file|
- diff_file.blob.path
+ expose :blob, using: BlobEntity
+
+ expose :can_modify_blob do |diff_file|
+ merge_request = options[:merge_request]
+
+ if merge_request&.source_project && current_user
+ can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
+ else
+ false
+ end
end
- expose :blob_icon do |diff_file|
- blob_icon(diff_file.b_mode, diff_file.file_path)
+ expose :file_hash do |diff_file|
+ Digest::SHA1.hexdigest(diff_file.file_path)
end
expose :file_path
+ expose :too_large?, as: :too_large
+ expose :collapsed?, as: :collapsed
+ expose :new_file?, as: :new_file
+
expose :deleted_file?, as: :deleted_file
expose :renamed_file?, as: :renamed_file
expose :old_path
@@ -28,6 +49,36 @@ class DiffFileEntity < Grape::Entity
expose :a_mode
expose :b_mode
expose :text?, as: :text
+ expose :added_lines
+ expose :removed_lines
+ expose :diff_refs
+ expose :content_sha
+ expose :stored_externally?, as: :stored_externally
+ expose :external_storage
+
+ expose :load_collapsed_diff_url, if: -> (diff_file, options) { diff_file.text? && options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+ project = merge_request.target_project
+
+ next unless project
+
+ diff_for_path_namespace_project_merge_request_path(
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid,
+ old_path: diff_file.old_path,
+ new_path: diff_file.new_path,
+ file_identifier: diff_file.file_identifier
+ )
+ end
+
+ expose :formatted_external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].formatted_external_url
+ end
+
+ expose :external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].external_url_for(diff_file.new_path, diff_file.content_sha)
+ end
expose :old_path_html do |diff_file|
old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
@@ -38,4 +89,64 @@ class DiffFileEntity < Grape::Entity
_, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
new_path
end
+
+ expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ options = merge_request.persisted? ? { from_merge_request_iid: merge_request.iid } : {}
+
+ next unless merge_request.source_project
+
+ project_edit_blob_path(merge_request.source_project,
+ tree_join(merge_request.source_branch, diff_file.new_path),
+ options)
+ end
+
+ expose :view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ project = merge_request.target_project
+
+ next unless project
+
+ project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
+ end
+
+ expose :replaced_view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image'
+ image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha
+
+ merge_request = options[:merge_request]
+ project = merge_request.target_project
+
+ next unless project
+
+ project_blob_path(project, tree_join(diff_file.old_content_sha, diff_file.old_path)) if image_diff && image_replaced
+ end
+
+ expose :context_lines_path, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ project_blob_diff_path(diff_file.repository.project, tree_join(diff_file.content_sha, diff_file.file_path))
+ end
+
+ # Used for inline diffs
+ expose :highlighted_diff_lines, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ diff_file.diff_lines_for_serializer
+ end
+
+ # Used for parallel diffs
+ expose :parallel_diff_lines, if: -> (diff_file, _) { diff_file.text? }
+
+ def current_user
+ request.current_user
+ end
+
+ def memoized_submodule_links(diff_file)
+ strong_memoize(:submodule_links) do
+ if diff_file.submodule?
+ submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository)
+ else
+ []
+ end
+ end
+ end
end
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
new file mode 100644
index 00000000000..bb804e5347a
--- /dev/null
+++ b/app/serializers/diffs_entity.rb
@@ -0,0 +1,65 @@
+class DiffsEntity < Grape::Entity
+ include DiffHelper
+ include RequestAwareEntity
+
+ expose :real_size
+ expose :size
+
+ expose :branch_name do |diffs|
+ merge_request&.source_branch
+ end
+
+ expose :target_branch_name do |diffs|
+ merge_request&.target_branch
+ end
+
+ expose :commit do |diffs|
+ options[:commit]
+ end
+
+ expose :merge_request_diff, using: MergeRequestDiffEntity do |diffs|
+ options[:merge_request_diff]
+ end
+
+ expose :start_version, using: MergeRequestDiffEntity do |diffs|
+ options[:start_version]
+ end
+
+ expose :latest_diff do |diffs|
+ options[:latest_diff]
+ end
+
+ expose :latest_version_path, if: -> (*) { merge_request } do |diffs|
+ diffs_project_merge_request_path(merge_request&.project, merge_request)
+ end
+
+ expose :added_lines do |diffs|
+ diffs.diff_files.sum(&:added_lines)
+ end
+
+ expose :removed_lines do |diffs|
+ diffs.diff_files.sum(&:removed_lines)
+ end
+
+ expose :render_overflow_warning do |diffs|
+ render_overflow_warning?(diffs.diff_files)
+ end
+
+ expose :email_patch_path, if: -> (*) { merge_request } do |diffs|
+ merge_request_path(merge_request, format: :patch)
+ end
+
+ expose :plain_diff_path, if: -> (*) { merge_request } do |diffs|
+ merge_request_path(merge_request, format: :diff)
+ end
+
+ expose :diff_files, using: DiffFileEntity
+
+ expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs|
+ options[:merge_request_diffs]
+ end
+
+ def merge_request
+ options[:merge_request]
+ end
+end
diff --git a/app/serializers/diffs_serializer.rb b/app/serializers/diffs_serializer.rb
new file mode 100644
index 00000000000..6771e10c5ac
--- /dev/null
+++ b/app/serializers/diffs_serializer.rb
@@ -0,0 +1,3 @@
+class DiffsSerializer < BaseSerializer
+ entity DiffsEntity
+end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index 718fb35e62d..63f28133a64 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -1,16 +1,31 @@
class DiscussionEntity < Grape::Entity
include RequestAwareEntity
+ include NotesHelper
expose :id, :reply_id
+ expose :position, if: -> (d, _) { d.diff_discussion? }
+ expose :line_code, if: -> (d, _) { d.diff_discussion? }
expose :expanded?, as: :expanded
+ expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? }
+ expose :project_id
expose :notes do |discussion, opts|
request.note_entity.represent(discussion.notes, opts)
end
+ expose :discussion_path do |discussion|
+ discussion_path(discussion)
+ end
+
expose :individual_note?, as: :individual_note
- expose :resolvable?, as: :resolvable
+ expose :resolvable do |discussion|
+ discussion.resolvable?
+ end
+
expose :resolved?, as: :resolved
+ expose :resolved_by_push?, as: :resolved_by_push
+ expose :resolved_by
+ expose :resolved_at
expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
end
@@ -18,24 +33,17 @@ class DiscussionEntity < Grape::Entity
new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
end
- expose :diff_file, using: DiffFileEntity, if: -> (d, _) { defined? d.diff_file }
+ expose :diff_file, using: DiffFileEntity, if: -> (d, _) { d.diff_discussion? }
expose :diff_discussion?, as: :diff_discussion
- expose :truncated_diff_lines, if: -> (d, _) { (defined? d.diff_file) && d.diff_file.text? } do |discussion|
- options[:context].render_to_string(
- partial: "projects/diffs/line",
- collection: discussion.truncated_diff_lines,
- as: :line,
- locals: { diff_file: discussion.diff_file,
- discussion_expanded: true,
- plain: true },
- layout: false,
- formats: [:html]
- )
+ expose :truncated_diff_lines_path, if: -> (d, _) { !d.expanded? && !render_truncated_diff_lines? } do |discussion|
+ project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion)
end
- expose :image_diff_html, if: -> (d, _) { defined? d.diff_file } do |discussion|
+ expose :truncated_diff_lines, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) }
+
+ expose :image_diff_html, if: -> (d, _) { d.diff_discussion? && d.on_image? } do |discussion|
diff_file = discussion.diff_file
partial = diff_file.new_file? || diff_file.deleted_file? ? 'single_image_diff' : 'replaced_image_diff'
options[:context].render_to_string(
@@ -47,4 +55,17 @@ class DiscussionEntity < Grape::Entity
formats: [:html]
)
end
+
+ expose :for_commit?, as: :for_commit
+ expose :commit_id
+
+ private
+
+ def render_truncated_diff_lines?
+ options[:render_truncated_diff_lines]
+ end
+
+ def current_user
+ request.current_user
+ end
end
diff --git a/app/serializers/merge_request_diff_entity.rb b/app/serializers/merge_request_diff_entity.rb
new file mode 100644
index 00000000000..32c761b45ac
--- /dev/null
+++ b/app/serializers/merge_request_diff_entity.rb
@@ -0,0 +1,46 @@
+class MergeRequestDiffEntity < Grape::Entity
+ include Gitlab::Routing
+ include GitHelper
+ include MergeRequestsHelper
+
+ expose :version_index do |merge_request_diff|
+ @merge_request_diffs = options[:merge_request_diffs]
+ diff = options[:merge_request_diff]
+
+ next unless diff.present?
+ next unless @merge_request_diffs.size > 1
+
+ version_index(merge_request_diff)
+ end
+
+ expose :created_at
+ expose :commits_count
+
+ expose :latest?, as: :latest
+
+ expose :short_commit_sha do |merge_request_diff|
+ short_sha(merge_request_diff.head_commit_sha)
+ end
+
+ expose :version_path do |merge_request_diff|
+ start_sha = options[:start_sha]
+ project = merge_request.target_project
+
+ next unless project
+
+ merge_request_version_path(project, merge_request, merge_request_diff, start_sha)
+ end
+
+ expose :compare_path do |merge_request_diff|
+ project = merge_request.target_project
+ diff = options[:merge_request_diff]
+
+ if project && diff
+ merge_request_version_path(project, merge_request, diff, merge_request_diff.head_commit_sha)
+ end
+ end
+
+ def merge_request
+ options[:merge_request]
+ end
+end
diff --git a/app/serializers/merge_request_user_entity.rb b/app/serializers/merge_request_user_entity.rb
new file mode 100644
index 00000000000..33fc7b724d5
--- /dev/null
+++ b/app/serializers/merge_request_user_entity.rb
@@ -0,0 +1,24 @@
+class MergeRequestUserEntity < UserEntity
+ include RequestAwareEntity
+ include BlobHelper
+ include TreeHelper
+
+ expose :can_fork do |user|
+ can?(user, :fork_project, request.project) if project
+ end
+
+ expose :can_create_merge_request do |user|
+ project && can?(user, :create_merge_request_in, project)
+ end
+
+ expose :fork_path, if: -> (*) { project } do |user|
+ params = edit_blob_fork_params("Edit")
+ project_forks_path(project, namespace_key: user.namespace.id, continue: params)
+ end
+
+ def project
+ return false unless request.respond_to?(:project) && request.project
+
+ request.project
+ end
+end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 8260c6c7b84..0426afc1b4a 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -120,12 +120,12 @@ class MergeRequestWidgetEntity < IssuableEntity
presenter(merge_request).can_cherry_pick_on_current_merge_request?
end
- expose :can_create_note do |issue|
- can?(request.current_user, :create_note, issue.project)
+ expose :can_create_note do |merge_request|
+ can?(request.current_user, :create_note, merge_request)
end
- expose :can_update do |issue|
- can?(request.current_user, :update_issue, issue)
+ expose :can_update do |merge_request|
+ can?(request.current_user, :update_merge_request, merge_request)
end
end
@@ -209,6 +209,10 @@ class MergeRequestWidgetEntity < IssuableEntity
commit_change_content_project_merge_request_path(merge_request.project, merge_request)
end
+ expose :preview_note_path do |merge_request|
+ preview_markdown_path(merge_request.project, quick_actions_target_type: 'MergeRequest', quick_actions_target_id: merge_request.id)
+ end
+
expose :merge_commit_path do |merge_request|
if merge_request.merge_commit_sha
project_commit_path(merge_request.project, merge_request.merge_commit_sha)
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index 06d603b277e..ce0c31b5806 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -1,5 +1,6 @@
class NoteEntity < API::Entities::Note
include RequestAwareEntity
+ include NotesHelper
expose :type
@@ -15,16 +16,21 @@ class NoteEntity < API::Entities::Note
expose :current_user do
expose :can_edit do |note|
- Ability.allowed?(request.current_user, :admin_note, note)
+ can?(current_user, :admin_note, note)
end
expose :can_award_emoji do |note|
- Ability.allowed?(request.current_user, :award_emoji, note)
+ can?(current_user, :award_emoji, note)
+ end
+
+ expose :can_resolve do |note|
+ note.resolvable? && can?(current_user, :resolve_note, note)
end
end
expose :resolved?, as: :resolved
expose :resolvable?, as: :resolvable
+
expose :resolved_by, using: NoteUserEntity
expose :system_note_icon_name, if: -> (note, _) { note.system? } do |note|
@@ -42,5 +48,23 @@ class NoteEntity < API::Entities::Note
new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note))
end
+ expose :noteable_note_url do |note|
+ noteable_note_url(note)
+ end
+
+ expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
+ end
+
+ expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
+ end
+
expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
+
+ private
+
+ def current_user
+ request.current_user
+ end
end
diff --git a/app/services/base_count_service.rb b/app/services/base_count_service.rb
index f2844854112..975e288301c 100644
--- a/app/services/base_count_service.rb
+++ b/app/services/base_count_service.rb
@@ -17,7 +17,7 @@ class BaseCountService
end
def refresh_cache(&block)
- Rails.cache.write(cache_key, block_given? ? yield : uncached_count, raw: raw?)
+ update_cache_for_key(cache_key, &block)
end
def uncached_count
@@ -41,4 +41,8 @@ class BaseCountService
def cache_options
{ raw: raw? }
end
+
+ def update_cache_for_key(key, &block)
+ Rails.cache.write(key, block_given? ? yield : uncached_count, raw: raw?)
+ end
end
diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb
index 26eb274f4d5..455f761ca9b 100644
--- a/app/services/concerns/issues/resolve_discussions.rb
+++ b/app/services/concerns/issues/resolve_discussions.rb
@@ -14,7 +14,6 @@ module Issues
def merge_request_to_resolve_discussions_of
strong_memoize(:merge_request_to_resolve_discussions_of) do
MergeRequestsFinder.new(current_user, project_id: project.id)
- .execute
.find_by(iid: merge_request_to_resolve_discussions_of_iid)
end
end
diff --git a/app/services/merge_requests/delete_non_latest_diffs_service.rb b/app/services/merge_requests/delete_non_latest_diffs_service.rb
new file mode 100644
index 00000000000..40079b21189
--- /dev/null
+++ b/app/services/merge_requests/delete_non_latest_diffs_service.rb
@@ -0,0 +1,18 @@
+module MergeRequests
+ class DeleteNonLatestDiffsService
+ BATCH_SIZE = 10
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ def execute
+ diffs = @merge_request.non_latest_diffs.with_files
+
+ diffs.each_batch(of: BATCH_SIZE) do |relation, index|
+ ids = relation.pluck(:id).map { |id| [id] }
+ DeleteDiffFilesWorker.bulk_perform_in(index * 5.minutes, ids)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb
deleted file mode 100644
index 10aa9ae609c..00000000000
--- a/app/services/merge_requests/merge_request_diff_cache_service.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module MergeRequests
- class MergeRequestDiffCacheService
- def execute(merge_request, new_diff)
- # Executing the iteration we cache all the highlighted diff information
- merge_request.diffs.diff_files.to_a
-
- # Remove cache for all diffs on this MR. Do not use the association on the
- # model, as that will interfere with other actions happening when
- # reloading the diff.
- MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
- next if merge_request_diff == new_diff
-
- merge_request_diff.diffs.clear_cache!
- end
- end
- end
-end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index c78e78afcd1..5b160ffba67 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -15,6 +15,7 @@ module MergeRequests
execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
+ delete_non_latest_diffs(merge_request)
end
private
@@ -31,6 +32,10 @@ module MergeRequests
end
end
+ def delete_non_latest_diffs(merge_request)
+ DeleteNonLatestDiffsService.new(merge_request).execute
+ end
+
def create_merge_event(merge_request, current_user)
EventCreateService.new.merge_mr(merge_request, current_user)
end
diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb
new file mode 100644
index 00000000000..2ec7b403903
--- /dev/null
+++ b/app/services/merge_requests/reload_diffs_service.rb
@@ -0,0 +1,43 @@
+module MergeRequests
+ class ReloadDiffsService
+ def initialize(merge_request, current_user)
+ @merge_request = merge_request
+ @current_user = current_user
+ end
+
+ def execute
+ old_diff_refs = merge_request.diff_refs
+ new_diff = merge_request.create_merge_request_diff
+
+ clear_cache(new_diff)
+ update_diff_discussion_positions(old_diff_refs)
+ end
+
+ private
+
+ attr_reader :merge_request, :current_user
+
+ def update_diff_discussion_positions(old_diff_refs)
+ new_diff_refs = merge_request.diff_refs
+
+ merge_request.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ current_user: current_user)
+ end
+
+ def clear_cache(new_diff)
+ # Executing the iteration we cache highlighted diffs for each diff file of
+ # MergeRequestDiff.
+ new_diff.diffs_collection.diff_files.to_a
+
+ # Remove cache for all diffs on this MR. Do not use the association on the
+ # model, as that will interfere with other actions happening when
+ # reloading the diff.
+ MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
+ next if merge_request_diff == new_diff
+
+ merge_request_diff.diffs_collection.clear_cache!
+ end
+ end
+ end
+end
diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb
index 933829b557b..4c8e000928f 100644
--- a/app/services/projects/count_service.rb
+++ b/app/services/projects/count_service.rb
@@ -22,8 +22,10 @@ module Projects
)
end
- def cache_key
- ['projects', 'count_service', VERSION, @project.id, cache_key_name]
+ def cache_key(key = nil)
+ cache_key = key || cache_key_name
+
+ ['projects', 'count_service', VERSION, @project.id, cache_key]
end
def self.query(project_ids)
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index 0a004677417..78b1477186a 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -4,6 +4,10 @@ module Projects
class OpenIssuesCountService < Projects::CountService
include Gitlab::Utils::StrongMemoize
+ # Cache keys used to store issues count
+ PUBLIC_COUNT_KEY = 'public_open_issues_count'.freeze
+ TOTAL_COUNT_KEY = 'total_open_issues_count'.freeze
+
def initialize(project, user = nil)
@user = user
@@ -11,7 +15,7 @@ module Projects
end
def cache_key_name
- public_only? ? 'public_open_issues_count' : 'total_open_issues_count'
+ public_only? ? PUBLIC_COUNT_KEY : TOTAL_COUNT_KEY
end
def public_only?
@@ -28,6 +32,32 @@ module Projects
end
end
+ def public_count_cache_key
+ cache_key(PUBLIC_COUNT_KEY)
+ end
+
+ def total_count_cache_key
+ cache_key(TOTAL_COUNT_KEY)
+ end
+
+ def refresh_cache(&block)
+ if block_given?
+ super(&block)
+ else
+ count_grouped_by_confidential = self.class.query(@project, public_only: false).group(:confidential).count
+ public_count = count_grouped_by_confidential[false] || 0
+ total_count = public_count + (count_grouped_by_confidential[true] || 0)
+
+ update_cache_for_key(public_count_cache_key) do
+ public_count
+ end
+
+ update_cache_for_key(total_count_cache_key) do
+ total_count
+ end
+ end
+ end
+
# We only show total issues count for reporters
# which are allowed to view confidential issues
# This will still show a discrepancy on issues number but should be less than before.
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 7ec52b6ce2b..8a86e47f0ea 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -82,7 +82,7 @@ class WebHookService
post_url = hook.url.gsub("#{parsed_url.userinfo}@", '')
basic_auth = {
username: CGI.unescape(parsed_url.user),
- password: CGI.unescape(parsed_url.password)
+ password: CGI.unescape(parsed_url.password.presence || '')
}
make_request(post_url, basic_auth)
end
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 38607ffca1c..bd43504dd37 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -324,3 +324,6 @@
= _('Configure push mirrors.')
.settings-content
= render partial: 'repository_mirrors_form'
+
+= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded
+
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index 7637471f9ae..ee2d4c8430a 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -10,16 +10,16 @@
.col-sm-10
= f.text_field :description, class: "form-control js-quick-submit"
.form-group.row
- = f.label :color, "Background color", class: 'col-form-label col-sm-2'
+ = f.label :color, _("Background color"), class: 'col-form-label col-sm-2'
.col-sm-10
.input-group
.input-group-prepend
.input-group-text.label-color-preview &nbsp;
= f.text_field :color, class: "form-control"
.form-text.text-muted
- Choose any color.
+ = _('Choose any color.')
%br
- Or you can choose one of the suggested colors below
+ = _("Or you can choose one of the suggested colors below")
.suggest-colors
- suggested_colors.each do |color|
@@ -27,5 +27,5 @@
&nbsp;
.form-actions
- = f.submit 'Save', class: 'btn btn-save js-save-button'
- = link_to "Cancel", admin_labels_path, class: 'btn btn-cancel'
+ = f.submit _('Save'), class: 'btn btn-save js-save-button'
+ = link_to _("Cancel"), admin_labels_path, class: 'btn btn-cancel'
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index 009a47dd517..c3ea2352898 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -3,5 +3,5 @@
= render_colored_label(label, tooltip: false)
= markdown_field(label, :description)
.float-right
- = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
- = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
+ = link_to _('Edit'), edit_admin_label_path(label), class: 'btn btn-sm'
+ = link_to _('Delete'), admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml
index 96f0d404ac4..652ed095d00 100644
--- a/app/views/admin/labels/edit.html.haml
+++ b/app/views/admin/labels/edit.html.haml
@@ -1,7 +1,7 @@
-- add_to_breadcrumbs "Labels", admin_labels_path
-- breadcrumb_title "Edit Label"
-- page_title "Edit", @label.name, "Labels"
+- add_to_breadcrumbs _("Labels"), admin_labels_path
+- breadcrumb_title _("Edit Label")
+- page_title _("Edit"), @label.name, _("Labels")
%h3.page-title
- Edit Label
+ = _('Edit Label')
%hr
= render 'form'
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index add38fb333e..d3e5247447a 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -1,10 +1,10 @@
-- page_title "Labels"
+- page_title _("Labels")
%div
= link_to new_admin_label_path, class: "float-right btn btn-nr btn-new" do
- New label
+ = _('New label')
%h3.page-title
- Labels
+ = _('Labels')
%hr
.labels
@@ -14,5 +14,5 @@
= paginate @labels, theme: 'gitlab'
- else
.card.bg-light
- .nothing-here-block There are no labels yet
+ .nothing-here-block= _('There are no labels yet')
diff --git a/app/views/admin/labels/new.html.haml b/app/views/admin/labels/new.html.haml
index 0135ad0723d..20103fb8a29 100644
--- a/app/views/admin/labels/new.html.haml
+++ b/app/views/admin/labels/new.html.haml
@@ -1,5 +1,5 @@
-- page_title "New Label"
+- page_title _("New Label")
%h3.page-title
- New Label
+ = _('New Label')
%hr
= render 'form'
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index c45d2214592..0ee563ac066 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -12,5 +12,9 @@
%span Remember me
.float-right.forgot-password
= link_to "Forgot your password?", new_password_path(:user)
+ %div
+ - if captcha_enabled?
+ = recaptcha_tags
+
.submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save"
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 087af61235b..58c585a29ff 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -3,8 +3,8 @@
%li.nav-item
= link_to "Crowd", "#crowd", class: 'nav-link active', 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
- %li.nav-item{ class: active_when(i.zero? && !crowd_enabled?) }
- = link_to server['label'], "##{server['provider_name']}", class: 'nav-link', 'data-toggle' => 'tab'
+ %li.nav-item
+ = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && !crowd_enabled?)}", 'data-toggle' => 'tab'
- if password_authentication_enabled_for_web?
%li.nav-item
= link_to 'Standard', '#login-pane', class: 'nav-link', 'data-toggle' => 'tab'
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 1e72d88db1e..53f54db1ddf 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -4,27 +4,37 @@
- page_title 'New Group'
- header_title "Groups", dashboard_groups_path
-%h3.page-title
- New Group
-%hr
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = _('New group')
+ %p
+ - group_docs_path = help_page_path('user/group/index')
+ - group_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_docs_path }
+ = s_('%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects.').html_safe % { group_docs_link_start: group_docs_link_start, group_docs_link_end: '</a>'.html_safe }
+ %p
+ - subgroup_docs_path = help_page_path('user/group/subgroups/index')
+ - subgroup_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: subgroup_docs_path }
+ = s_('Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}.').html_safe % { subgroup_docs_link_start: subgroup_docs_link_start, subgroup_docs_link_end: '</a>'.html_safe }
-= form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f|
- = form_errors(@group)
- = render 'shared/group_form', f: f, autofocus: true
+ .col-lg-9
+ = form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f|
+ = form_errors(@group)
+ = render 'shared/group_form', f: f, autofocus: true
- .form-group.row.group-description-holder
- = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
- .col-sm-10
- = render 'shared/choose_group_avatar_button', f: f
+ .form-group.row.group-description-holder
+ = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
+ .col-sm-10
+ = render 'shared/choose_group_avatar_button', f: f
- = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group
+ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group
- = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
+ = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
- .form-group.row
- .offset-sm-2.col-sm-10
- = render 'shared/group_tips'
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ = render 'shared/group_tips'
- .form-actions
- = f.submit 'Create group', class: "btn btn-create"
- = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
+ .form-actions
+ = f.submit 'Create group', class: "btn btn-create"
+ = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 24b6c490a5a..a74ea246eaf 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -17,6 +17,11 @@
= link_to _("Help"), help_path
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
%li.divider
+ %li
+ = link_to "https://about.gitlab.com/contributing", target: '_blank', class: 'text-nowrap' do
+ = _("Contribute to GitLab")
+ = sprite_icon('external-link', size: 16)
+ %li.divider
- if current_user_menu?(:sign_out)
%li
= link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link"
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 9f8b3b86474..33416bf76d7 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -8,7 +8,7 @@
.sidebar-context-title
= @project.name
%ul.sidebar-top-level-items
- = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
+ = nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
= link_to project_path(@project), class: 'shortcuts-project' do
.nav-icon-container
= sprite_icon('project')
@@ -29,13 +29,13 @@
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
%span= _('Activity')
+ = render_if_exists 'projects/sidebar/security_dashboard'
+
- if can?(current_user, :read_cycle_analytics, @project)
= nav_link(path: 'cycle_analytics#show') do
= link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do
%span= _('Cycle Analytics')
- = render_if_exists 'projects/sidebar/security_dashboard'
-
- if project_nav_tab? :files
= nav_link(controller: sidebar_repository_paths) do
= link_to project_tree_path(@project), class: 'shortcuts-tree' do
diff --git a/app/views/notify/merge_request_unmergeable_email.html.haml b/app/views/notify/merge_request_unmergeable_email.html.haml
index 578fa1fbce7..7ec0c1ef390 100644
--- a/app/views/notify/merge_request_unmergeable_email.html.haml
+++ b/app/views/notify/merge_request_unmergeable_email.html.haml
@@ -1,6 +1,2 @@
%p
- Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to the following #{'reason'.pluralize(@reasons.count)}:
-
- %ul
- - @reasons.each do |reason|
- %li= reason
+ Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to conflict.
diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml
index e4f9f1bf5e7..dcdd6db69d6 100644
--- a/app/views/notify/merge_request_unmergeable_email.text.haml
+++ b/app/views/notify/merge_request_unmergeable_email.text.haml
@@ -1,7 +1,4 @@
-Merge Request #{@merge_request.to_reference} can no longer be merged due to the following #{'reason'.pluralize(@reasons.count)}:
-
-- @reasons.each do |reason|
- * #{reason}
+Merge Request #{@merge_request.to_reference} can no longer be merged due to conflict.
Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 6ea358d9f63..c14700794ce 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -4,10 +4,12 @@
.form-group
= f.label :key, class: 'label-light'
- = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the SSH key. Paste the public part, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'."
+ %p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.")
+ = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: 'Typically starts with "ssh-rsa …"'
.form-group
= f.label :title, class: 'label-light'
- = f.text_field :title, class: "form-control", required: true
+ = f.text_field :title, class: "form-control", required: true, placeholder: 'e.g. My MacBook key'
+ %p.form-text.text-muted= _('Name your individual key via a title')
.prepend-top-default
= f.submit 'Add key', class: "btn btn-create"
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 1e206def7ee..55ca8d0ebd4 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -11,10 +11,11 @@
%h5.prepend-top-0
Add an SSH key
%p.profile-settings-content
- Before you can add an SSH key you need to
- = link_to "generate one", help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
- or use an
- = link_to "existing key.", help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
+ - generate_link_url = help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
+ - existing_link_url = help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
+ - generate_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: generate_link_url }
+ - existing_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: existing_link_url }
+ = _('To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}.').html_safe % { generate_link_start: generate_link_start, existing_link_start: existing_link_start, link_end: '</a>'.html_safe }
= render 'form'
%hr
%h5
diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
index d0402197821..9298d93663d 100644
--- a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
@@ -6,7 +6,7 @@
= image_tag 'illustrations/logos/google-cloud-platform_logo.svg'
.col-sm-10
%h4= s_('ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform')
- %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
+ %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
%a.btn.btn-info{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' }
Apply for credit
diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml
index db97203a2aa..b46b45fea49 100644
--- a/app/views/projects/clusters/_integration_form.html.haml
+++ b/app/views/projects/clusters/_integration_form.html.haml
@@ -1,6 +1,6 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
- .form-group.append-bottom-20
+ .form-group
%h5= s_('ClusterIntegration|Integration status')
%p
- if @cluster.enabled?
@@ -10,7 +10,7 @@
= s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project.')
- else
= s_('ClusterIntegration|Kubernetes cluster integration is disabled for this project.')
- %label.append-bottom-10.js-cluster-enable-toggle-area
+ %label.append-bottom-0.js-cluster-enable-toggle-area
%button{ type: 'button',
class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
"aria-label": s_("ClusterIntegration|Toggle Kubernetes cluster"),
@@ -20,19 +20,26 @@
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
- .form-group
- %h5= s_('ClusterIntegration|Security')
- %p
- = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.")
- = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
-
- .form-group
- %h5= s_('ClusterIntegration|Environment scope')
- %p
- = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
- = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
- = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
+ - if has_multiple_clusters?(@project)
+ .form-group
+ %h5= s_('ClusterIntegration|Environment scope')
+ %p
+ = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
+ = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
+ = field.text_field :environment_scope, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
- if can?(current_user, :update_cluster, @cluster)
.form-group
= field.submit _('Save changes'), class: 'btn btn-success'
+
+ - unless has_multiple_clusters?(@project)
+ %h5= s_('ClusterIntegration|Environment scope')
+ %p
+ %code *
+ is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster.
+ = link_to 'More information', ('https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope')
+
+ %h5= s_('ClusterIntegration|Security')
+ %p
+ = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.")
+ = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index c7ac687e4a6..282566eeadc 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -14,4 +14,4 @@
= author_avatar(deployment.commit, size: 20)
= link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message"
- else
- Cant find HEAD commit for this branch
+ = _("Can't find HEAD commit for this branch")
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index 520696b01c6..85bc8ec07e3 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -1,14 +1,14 @@
.gl-responsive-table-row.deployment{ role: 'row' }
.table-section.section-10{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } ID
+ .table-mobile-header{ role: 'rowheader' }= _("ID")
%strong.table-mobile-content ##{deployment.iid}
.table-section.section-30{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } Commit
+ .table-mobile-header{ role: 'rowheader' }= _("Commit")
= render 'projects/deployments/commit', deployment: deployment
.table-section.section-25.build-column{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } Job
+ .table-mobile-header{ role: 'rowheader' }= _("Job")
- if deployment.deployable
.table-mobile-content
.flex-truncate-parent
@@ -21,7 +21,7 @@
= user_avatar(user: deployment.user, size: 20)
.table-section.section-15{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } Created
+ .table-mobile-header{ role: 'rowheader' }= _("Created")
%span.table-mobile-content= time_ago_with_tooltip(deployment.created_at)
.table-section.section-20.table-button-footer{ role: 'gridcell' }
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
index 5941e01c6f1..95f950948ab 100644
--- a/app/views/projects/deployments/_rollback.haml
+++ b/app/views/projects/deployments/_rollback.haml
@@ -1,6 +1,6 @@
- if can?(current_user, :create_deployment, deployment) && deployment.deployable
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
- if deployment.last?
- Re-deploy
+ = _("Re-deploy")
- else
- Rollback
+ = _("Rollback")
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index 983cb187c2f..3f1974d05f4 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -30,7 +30,7 @@
#{@commits_graph.start_date.strftime('%b %d')}
- end_time = capture do
#{@commits_graph.end_date.strftime('%b %d')}
- = (_("Commit statistics for %{ref} %{start_time} - %{end_time}") % { ref: "<strong>#{@ref}</strong>", start_time: start_time, end_time: end_time }).html_safe
+ = (_("Commit statistics for %{ref} %{start_time} - %{end_time}") % { ref: "<strong>#{h @ref}</strong>", start_time: start_time, end_time: end_time }).html_safe
.col-md-6
.tree-ref-container
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 816f2fa816d..665968a64e1 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -8,5 +8,6 @@
%section.js-vue-notes-event
#js-vue-notes{ data: { notes_data: notes_data(@issue),
noteable_data: serialize_issuable(@issue),
- noteable_type: 'issue',
+ noteable_type: 'Issue',
+ target_type: 'issue',
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml
index 19659fe5140..bf3df0abf86 100644
--- a/app/views/projects/merge_requests/diffs/_diffs.html.haml
+++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml
@@ -16,6 +16,6 @@
%span.ref-name= @merge_request.target_branch
.text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save'
- else
- - diff_viewable = @merge_request_diff ? @merge_request_diff.collected? || @merge_request_diff.overflow? : true
+ - diff_viewable = @merge_request_diff ? @merge_request_diff.viewable? : true
- if diff_viewable
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 2f1877a15c2..b23baa22d8b 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -52,24 +52,7 @@
Changes
%span.badge.badge-pill= @merge_request.diff_size
- - if has_vue_discussions_cookie?
- #js-vue-discussion-counter
- - else
- #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
- %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
- %div
- .line-resolve-all{ "v-show" => "discussionCount > 0",
- ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
- %span.line-resolve-btn.is-disabled{ type: "button",
- ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- %template{ 'v-if' => 'resolvedDiscussionCount === discussionCount' }
- = render 'shared/icons/icon_status_success_solid.svg'
- %template{ 'v-else' => '' }
- = render 'shared/icons/icon_resolve_discussion.svg'
- %span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
- = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
- = render "discussions/jump_to_next"
+ #js-vue-discussion-counter
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
@@ -77,20 +60,21 @@
%section.col-md-12
%script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe
.issuable-discussion.js-vue-notes-event
- = render "projects/merge_requests/discussion"
- - if has_vue_discussions_cookie?
- #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
- noteable_data: serialize_issuable(@merge_request),
- noteable_type: 'merge_request',
- current_user_data: UserSerializer.new.represent(current_user).to_json} }
+ #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
+ noteable_data: serialize_issuable(@merge_request),
+ noteable_type: 'MergeRequest',
+ target_type: 'merge_request',
+ current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json} }
#commits.commits.tab-pane
-# This tab is always loaded via AJAX
#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{ data: { "is-locked" => @merge_request.discussion_locked? } }
- -# This tab is always loaded via AJAX
+ #js-diffs-app.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked?,
+ endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', request.query_parameters),
+ current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json,
+ project_path: project_path(@merge_request.project)} }
.mr-loading-status
= spinner
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 496b94ec953..a88d8f61fb4 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -3,8 +3,8 @@
- @no_breadcrumb_container = true
- @no_container = true
- @content_class = "issue-boards-content"
-- breadcrumb_title "Issue Board"
-- page_title "Boards"
+- breadcrumb_title _("Issue Board")
+- page_title _("Boards")
- content_for :page_specific_javascripts do
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 76843ce7cc0..65de6172d89 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -30,7 +30,7 @@
%board-delete{ "inline-template" => true,
":list" => "list",
"v-if" => "!list.preset && list.id" }
- %button.board-delete.has-tooltip.float-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
+ %button.board-delete.has-tooltip.float-right{ type: "button", title: _("Delete list"), "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
.issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' }
%span.issue-count-badge-count.float-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
@@ -39,8 +39,8 @@
%button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"',
- "aria-label" => "New issue",
- "title" => "New issue",
+ "aria-label" => _("New issue"),
+ "title" => _("New issue"),
data: { placement: "top", container: "body" } }
= icon("plus", class: "js-no-trigger-collapse")
diff --git a/app/views/shared/boards/components/sidebar/_due_date.html.haml b/app/views/shared/boards/components/sidebar/_due_date.html.haml
index 10217b6cbf0..5630375f428 100644
--- a/app/views/shared/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/shared/boards/components/sidebar/_due_date.html.haml
@@ -1,20 +1,20 @@
.block.due_date
.title
- Due date
+ = _("Due date")
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value
.value-content
%span.no-value{ "v-if" => "!issue.dueDate" }
- No due date
+ = _("No due date")
%span.bold{ "v-if" => "issue.dueDate" }
{{ issue.dueDate | due-date }}
- if can_admin_issue?
%span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
\-
%a.js-remove-due-date{ href: "#", role: "button" }
- remove due date
+ = _('remove due date')
- if can_admin_issue?
.selectbox
%input{ type: "hidden",
@@ -23,9 +23,9 @@
.dropdown
%button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" } }
- %span.dropdown-toggle-text Due date
+ %span.dropdown-toggle-text= _("Due date")
= icon('chevron-down')
.dropdown-menu.dropdown-menu-due-date
- = dropdown_title('Due date')
+ = dropdown_title(_('Due date'))
= dropdown_content do
.js-due-date-calendar
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index daee691e358..607e7f471c9 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -1,12 +1,12 @@
.block.labels
.title
- Labels
+ = _("Labels")
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value.issuable-show-labels.dont-hide
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
- None
+ = _("None")
%a{ href: "#",
"v-for" => "label in issue.labels" }
.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
@@ -28,7 +28,7 @@
namespace_path: @namespace_path,
project_path: @project.try(:path) } }
%span.dropdown-toggle-text
- Label
+ = _("Label")
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default"
diff --git a/app/views/shared/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml
index f2bedd5e3c9..b15d60002fc 100644
--- a/app/views/shared/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml
@@ -1,12 +1,12 @@
.block.milestone
.title
- Milestone
+ = _("Milestone")
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value
%span.no-value{ "v-if" => "!issue.milestone" }
- None
+ = _("None")
%span.bold.has-tooltip{ "v-if" => "issue.milestone" }
{{ issue.milestone.title }}
- if can_admin_issue?
@@ -19,10 +19,10 @@
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
":data-selected" => "milestoneTitle",
":data-issuable-id" => "issue.iid" }
- Milestone
+ = _("Milestone")
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable
- = dropdown_title("Assign milestone")
- = dropdown_filter("Search milestones")
+ = dropdown_title(_("Assign milestone"))
+ = dropdown_filter(_("Search milestones"))
= dropdown_content
= dropdown_loading
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index c360f1ffe2a..6b2715b47a7 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -40,5 +40,5 @@
= yield(:note_actions)
- %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
+ %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Discard draft" } }
Discard draft
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 526330f4e50..d4e8f30e458 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -59,7 +59,7 @@
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
- .system-note-commit-list-toggler
+ .system-note-commit-list-toggler.hide
Toggle commit list
%i.fa.fa-angle-down
- if note.attachment.url
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index b98d5339d2d..e0832fd9136 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -1,14 +1,13 @@
- issuable = @issue || @merge_request
- discussion_locked = issuable&.discussion_locked?
-- unless has_vue_discussions_cookie?
- %ul#notes-list.notes.main-notes-list.timeline
- = render "shared/notes/notes"
+%ul#notes-list.notes.main-notes-list.timeline
+ = render "shared/notes/notes"
= render 'shared/notes/edit_form', project: @project
- if can_create_note?
- %ul.notes.notes-form.timeline{ :class => ('hidden' if has_vue_discussions_cookie?) }
+ %ul.notes.notes-form.timeline
%li.timeline-entry
.timeline-entry-inner
.flash-container.timeline-content
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index e50b7fa68dd..96527fcb4f2 100644
--- a/app/views/shared/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -21,17 +21,17 @@
%th Value
%tr
%td Active
- %td= @runner.active? ? _('Yes') : _('No')
+ %td= @runner.active? ? 'Yes' : 'No'
%tr
%td Protected
- %td= @runner.ref_protected? ? _('Yes') : _('No')
+ %td= @runner.active? ? _('Yes') : _('No')
%tr
- %td= _('Can run untagged jobs')
- %td= @runner.run_untagged? ? _('Yes') : _('No')
+ %td Can run untagged jobs
+ %td= @runner.run_untagged? ? 'Yes' : 'No'
- unless @runner.group_type?
%tr
- %td= _('Locked to this project')
- %td= @runner.locked? ? _('Yes') : _('No')
+ %td Locked to this project
+ %td= @runner.locked? ? 'Yes' : 'No'
%tr
%td Tags
%td
@@ -60,7 +60,7 @@
%td Description
%td= @runner.description
%tr
- %td= _('Maximum job timeout')
+ %td Maximum job timeout
%td= @runner.maximum_timeout_human_readable
%tr
%td Last contact
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index 2d0bb722189..e5c82962f82 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -6,5 +6,4 @@
%fieldset
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
= label_tag ("#{prefix}_scopes_#{scope}"), scope, class: "label-light"
- %span= t(scope, scope: [:doorkeeper, :scopes])
.scope-description= t scope, scope: [:doorkeeper, :scope_desc]
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index 7eb221620ad..1c788b9a737 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -2,9 +2,6 @@
%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code
-# haml-lint:disable InlineJavaScript
-%script#js-authenticate-u2f-not-supported{ type: "text/template" }
- %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
-
%script#js-authenticate-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 30b6796a7d6..026f756582d 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -118,3 +118,4 @@
- web_hook
- repository_update_remote_mirror
- create_note_diff_file
+- delete_diff_files
diff --git a/app/workers/delete_diff_files_worker.rb b/app/workers/delete_diff_files_worker.rb
new file mode 100644
index 00000000000..bb8fbb9c373
--- /dev/null
+++ b/app/workers/delete_diff_files_worker.rb
@@ -0,0 +1,17 @@
+class DeleteDiffFilesWorker
+ include ApplicationWorker
+
+ def perform(merge_request_diff_id)
+ merge_request_diff = MergeRequestDiff.find(merge_request_diff_id)
+
+ return if merge_request_diff.without_files?
+
+ MergeRequestDiff.transaction do
+ merge_request_diff.clean!
+
+ MergeRequestDiffFile
+ .where(merge_request_diff_id: merge_request_diff.id)
+ .delete_all
+ end
+ end
+end
diff --git a/bin/changelog b/bin/changelog
index 9b60f53ce40..d7b2a1a2de9 100755
--- a/bin/changelog
+++ b/bin/changelog
@@ -19,7 +19,24 @@ Options = Struct.new(
)
INVALID_TYPE = -1
+module ChangelogHelpers
+ Abort = Class.new(StandardError)
+ Done = Class.new(StandardError)
+
+ def capture_stdout(cmd)
+ output = IO.popen(cmd, &:read)
+ fail_with "command failed: #{cmd.join(' ')}" unless $?.success?
+ output
+ end
+
+ def fail_with(message)
+ raise Abort, "\e[31merror\e[0m #{message}"
+ end
+end
+
class ChangelogOptionParser
+ extend ChangelogHelpers
+
Type = Struct.new(:name, :description)
TYPES = [
Type.new('added', 'New feature'),
@@ -68,7 +85,7 @@ class ChangelogOptionParser
opts.on('-h', '--help', 'Print help message') do
$stdout.puts opts
- exit
+ raise Done.new
end
end
@@ -108,18 +125,19 @@ class ChangelogOptionParser
def assert_valid_type!(type)
unless type
- $stderr.puts "Invalid category index, please select an index between 1 and #{TYPES.length}"
- exit 1
+ raise Abort, "Invalid category index, please select an index between 1 and #{TYPES.length}"
end
end
def git_user_name
- %x{git config user.name}.strip
+ capture_stdout(%w[git config user.name]).strip
end
end
end
class ChangelogEntry
+ include ChangelogHelpers
+
attr_reader :options
def initialize(options)
@@ -159,13 +177,9 @@ class ChangelogEntry
end
def amend_commit
- %x{git add #{file_path}}
- exec("git commit --amend")
- end
+ fail_with "git add failed" unless system(*%W[git add #{file_path}])
- def fail_with(message)
- $stderr.puts "\e[31merror\e[0m #{message}"
- exit 1
+ Kernel.exec(*%w[git commit --amend])
end
def assert_feature_branch!
@@ -203,7 +217,7 @@ class ChangelogEntry
end
def last_commit_subject
- %x{git log --format="%s" -1}.strip
+ capture_stdout(%w[git log --format=%s -1]).strip
end
def file_path
@@ -225,7 +239,7 @@ class ChangelogEntry
end
def branch_name
- @branch_name ||= %x{git symbolic-ref --short HEAD}.strip
+ @branch_name ||= capture_stdout(%w[git symbolic-ref --short HEAD]).strip
end
def remove_trailing_whitespace(yaml_content)
@@ -234,8 +248,15 @@ class ChangelogEntry
end
if $0 == __FILE__
- options = ChangelogOptionParser.parse(ARGV)
- ChangelogEntry.new(options)
+ begin
+ options = ChangelogOptionParser.parse(ARGV)
+ ChangelogEntry.new(options)
+ rescue ChangelogHelpers::Abort => ex
+ $stderr.puts ex.message
+ exit 1
+ rescue ChangelogHelpers::Done
+ exit
+ end
end
# vim: ft=ruby
diff --git a/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml b/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml
deleted file mode 100644
index 9287243a7e3..00000000000
--- a/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix double-brackets being linkified in wiki markdown
-merge_request: 18524
-author: brewingcode
-type: fixed
diff --git a/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml
deleted file mode 100644
index a97e8a2b5cc..00000000000
--- a/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add API endpoint to render markdown text
-merge_request: 18926
-author: "@blackst0ne"
-type: added
diff --git a/changelogs/unreleased/22647-width-contributors-graphs.yml b/changelogs/unreleased/22647-width-contributors-graphs.yml
deleted file mode 100644
index 87be3a25d8a..00000000000
--- a/changelogs/unreleased/22647-width-contributors-graphs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix width of contributors graphs
-merge_request: 18639
-author: Paul Vorbach
-type: fixed
diff --git a/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml b/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml
deleted file mode 100644
index 2b4727c5f03..00000000000
--- a/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix an issue where the notification email address would be set to an unconfirmed
- email address
-merge_request: 18474
-author:
-type: fixed
diff --git a/changelogs/unreleased/23465-print-markdown.yml b/changelogs/unreleased/23465-print-markdown.yml
deleted file mode 100644
index ba950667acc..00000000000
--- a/changelogs/unreleased/23465-print-markdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix print styles for markdown pages
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/25045-add-variables-to-post-pipeline-api.yml b/changelogs/unreleased/25045-add-variables-to-post-pipeline-api.yml
deleted file mode 100644
index 1e648b75248..00000000000
--- a/changelogs/unreleased/25045-add-variables-to-post-pipeline-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add variables to POST api/v4/projects/:id/pipeline
-merge_request: 19124
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/25955-update-404-pages.yml b/changelogs/unreleased/25955-update-404-pages.yml
deleted file mode 100644
index 121229a77b9..00000000000
--- a/changelogs/unreleased/25955-update-404-pages.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update 404 and 403 pages with helpful actions.
-merge_request: 19096
-author:
-type: changed
diff --git a/changelogs/unreleased/36862-subgroup-milestones.yml b/changelogs/unreleased/36862-subgroup-milestones.yml
deleted file mode 100644
index 98b9dc41cb1..00000000000
--- a/changelogs/unreleased/36862-subgroup-milestones.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Include milestones from parent groups when assigning a milestone to an issue or merge request
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/38542-application-control-panel-in-settings-page.yml b/changelogs/unreleased/38542-application-control-panel-in-settings-page.yml
deleted file mode 100644
index 0654456ea45..00000000000
--- a/changelogs/unreleased/38542-application-control-panel-in-settings-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add deploy strategies to the Auto DevOps settings
-merge_request: 19172
-author:
-type: added
diff --git a/changelogs/unreleased/38759-fetch-available-parameters-directly-from-gke-when-creating-a-cluster.yml b/changelogs/unreleased/38759-fetch-available-parameters-directly-from-gke-when-creating-a-cluster.yml
deleted file mode 100644
index e7d0d37becd..00000000000
--- a/changelogs/unreleased/38759-fetch-available-parameters-directly-from-gke-when-creating-a-cluster.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Dynamically fetch GCP cluster creation parameters.
-merge_request: 17806
-author:
-type: changed
diff --git a/changelogs/unreleased/38919-wiki-empty-states.yml b/changelogs/unreleased/38919-wiki-empty-states.yml
deleted file mode 100644
index 953fa29e659..00000000000
--- a/changelogs/unreleased/38919-wiki-empty-states.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add helpful messages to empty wiki view
-merge_request: 19007
-author:
-type: other
diff --git a/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml b/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml
deleted file mode 100644
index fb4fbf80575..00000000000
--- a/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Label list page redesign
-merge_request: 18466
-author:
-type: changed
diff --git a/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml
deleted file mode 100644
index 9f07fcdfa0b..00000000000
--- a/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Apply NestingDepth (level 5) (pages/pipelines.scss)
-merge_request: 18830
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/39710-search-placeholder-cut-off.yml b/changelogs/unreleased/39710-search-placeholder-cut-off.yml
deleted file mode 100644
index 59290768c6a..00000000000
--- a/changelogs/unreleased/39710-search-placeholder-cut-off.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Fixes: Runners search input placeholder is cut off'
-merge_request: 19015
-author: Jacopo Beschi @jacopo-beschi
-type: fixed
diff --git a/changelogs/unreleased/40005-u2f-unspported-browsers.yml b/changelogs/unreleased/40005-u2f-unspported-browsers.yml
new file mode 100644
index 00000000000..eb5ff99246e
--- /dev/null
+++ b/changelogs/unreleased/40005-u2f-unspported-browsers.yml
@@ -0,0 +1,5 @@
+---
+title: Improve U2F workflow when using unsupported browsers
+merge_request: 19938
+author: Jan Beckmann
+type: changed
diff --git a/changelogs/unreleased/40725-move-mr-external-link-to-right.yml b/changelogs/unreleased/40725-move-mr-external-link-to-right.yml
deleted file mode 100644
index e3ebeb5eb61..00000000000
--- a/changelogs/unreleased/40725-move-mr-external-link-to-right.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Moves MR widget external link icon to the right
-merge_request: 18828
-author: Jacopo Beschi @jacopo-beschi
-type: changed
diff --git a/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml b/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml
deleted file mode 100644
index 58686639594..00000000000
--- a/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: made listing and showing public issue apis available without authentication
-merge_request: 18638
-author: haseebeqx
-type: changed
diff --git a/changelogs/unreleased/41587-osw-mr-metrics-migration-cleanup.yml b/changelogs/unreleased/41587-osw-mr-metrics-migration-cleanup.yml
deleted file mode 100644
index f953d380808..00000000000
--- a/changelogs/unreleased/41587-osw-mr-metrics-migration-cleanup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Take two for MR metrics population background migration
-merge_request: 19097
-author:
-type: other
diff --git a/changelogs/unreleased/42531-open-invite-404.yml b/changelogs/unreleased/42531-open-invite-404.yml
deleted file mode 100644
index 73729f4a929..00000000000
--- a/changelogs/unreleased/42531-open-invite-404.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Automatically accepts project/group invite by email after user signup
-merge_request: 17634
-author: Jacopo Beschi @jacopo-beschi
-type: changed
diff --git a/changelogs/unreleased/42751-rename-master-to-maintainer.yml b/changelogs/unreleased/42751-rename-master-to-maintainer.yml
deleted file mode 100644
index d7f499ecd52..00000000000
--- a/changelogs/unreleased/42751-rename-master-to-maintainer.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rename the Master role to Maintainer
-merge_request: 19080
-author:
-type: changed
diff --git a/changelogs/unreleased/42751-rename-mr-maintainer-push.yml b/changelogs/unreleased/42751-rename-mr-maintainer-push.yml
deleted file mode 100644
index aa29f6ed4b7..00000000000
--- a/changelogs/unreleased/42751-rename-mr-maintainer-push.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rephrasing Merge Request's 'allow edits from maintainer' functionality
-merge_request: 19061
-author:
-type: deprecated
diff --git a/changelogs/unreleased/43367-fix-board-long-strings.yml b/changelogs/unreleased/43367-fix-board-long-strings.yml
deleted file mode 100644
index 6228741601e..00000000000
--- a/changelogs/unreleased/43367-fix-board-long-strings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix issue board bug with long strings in titles
-merge_request: 18924
-author:
-type: fixed
diff --git a/changelogs/unreleased/43597-new-navigation-themes.yml b/changelogs/unreleased/43597-new-navigation-themes.yml
deleted file mode 100644
index de703e46b3c..00000000000
--- a/changelogs/unreleased/43597-new-navigation-themes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add additional theme color options
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/43673-operations-tab-mvc.yml b/changelogs/unreleased/43673-operations-tab-mvc.yml
deleted file mode 100644
index cd580e7a8d6..00000000000
--- a/changelogs/unreleased/43673-operations-tab-mvc.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move project sidebar sub-entries 'Environments' and 'Kubernetes' from 'CI/CD' to a new entry 'Operations'
-merge_request: 18941
-author:
-type: changed
diff --git a/changelogs/unreleased/44184-issues_ical_feed.yml b/changelogs/unreleased/44184-issues_ical_feed.yml
deleted file mode 100644
index 8151d82625a..00000000000
--- a/changelogs/unreleased/44184-issues_ical_feed.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Export assigned issues in iCalendar feed
-merge_request: 17783
-author: Imre Farkas
-type: added
diff --git a/changelogs/unreleased/44267-improve-failed-jobs-tab.yml b/changelogs/unreleased/44267-improve-failed-jobs-tab.yml
deleted file mode 100644
index 9743704e23d..00000000000
--- a/changelogs/unreleased/44267-improve-failed-jobs-tab.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve Failed Jobs tab in the Pipeline detail page
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/44319-remove-gray-buttons.yml b/changelogs/unreleased/44319-remove-gray-buttons.yml
deleted file mode 100644
index 9803dde8493..00000000000
--- a/changelogs/unreleased/44319-remove-gray-buttons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove gray button styles
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/44579-ide-add-pipeline-to-status-bar.yml b/changelogs/unreleased/44579-ide-add-pipeline-to-status-bar.yml
deleted file mode 100644
index 21e7c795815..00000000000
--- a/changelogs/unreleased/44579-ide-add-pipeline-to-status-bar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add pipeline status to the status bar of the Web IDE
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/44790-disabled-emails-logging.yml b/changelogs/unreleased/44790-disabled-emails-logging.yml
deleted file mode 100644
index 90125dc0300..00000000000
--- a/changelogs/unreleased/44790-disabled-emails-logging.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Stop logging email information when emails are disabled
-merge_request: 18521
-author: Marc Shaw
-type: fixed
diff --git a/changelogs/unreleased/44799-api-naming-issue-scope.yml b/changelogs/unreleased/44799-api-naming-issue-scope.yml
deleted file mode 100644
index 75c6ea4cd0d..00000000000
--- a/changelogs/unreleased/44799-api-naming-issue-scope.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rename issue scope created-by-me to created_by_me, and assigned-to-me to assigned_to_me
-merge_request: 44799
-author:
-type: deprecated
diff --git a/changelogs/unreleased/45065-users-projects-json-sort.yml b/changelogs/unreleased/45065-users-projects-json-sort.yml
deleted file mode 100644
index 89a1d7eb36f..00000000000
--- a/changelogs/unreleased/45065-users-projects-json-sort.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Order UsersController#projects.json by updated_at
-merge_request: 18227
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/45190-create-notes-diff-files.yml b/changelogs/unreleased/45190-create-notes-diff-files.yml
deleted file mode 100644
index efe322b682d..00000000000
--- a/changelogs/unreleased/45190-create-notes-diff-files.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Persist truncated note diffs on a new table
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/45404-remove-gemnasium-badge-from-project-s-readme-md.yml b/changelogs/unreleased/45404-remove-gemnasium-badge-from-project-s-readme-md.yml
deleted file mode 100644
index af2cb65445c..00000000000
--- a/changelogs/unreleased/45404-remove-gemnasium-badge-from-project-s-readme-md.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove Gemnasium badge from project README.md
-merge_request: 19136
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml b/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml
deleted file mode 100644
index 0694206d4fb..00000000000
--- a/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Updates updated_at on issuable when setting time spent
-merge_request: 18757
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/45505-lograge_formatter_encoding.yml b/changelogs/unreleased/45505-lograge_formatter_encoding.yml
deleted file mode 100644
index 02f4c152966..00000000000
--- a/changelogs/unreleased/45505-lograge_formatter_encoding.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Enforce UTF-8 encoding on user input in LogrageWithTimestamp formatter and
- filter out file content from logs
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/45520-remove-links-from-web-ide.yml b/changelogs/unreleased/45520-remove-links-from-web-ide.yml
deleted file mode 100644
index 81d5c26992f..00000000000
--- a/changelogs/unreleased/45520-remove-links-from-web-ide.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Change the IDE file buttons for an "Open in file view" button
-merge_request: 19129
-author: Sam Beckham
-type: changed
diff --git a/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml b/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml
deleted file mode 100644
index 31b4c29e03d..00000000000
--- a/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display help text below auto devops domain with nip.io domain name (#45561)
-merge_request: 18496
-author:
-type: added
diff --git a/changelogs/unreleased/45702-fix-hashed-storage-repository-archive.yml b/changelogs/unreleased/45702-fix-hashed-storage-repository-archive.yml
deleted file mode 100644
index 0f85ced06a9..00000000000
--- a/changelogs/unreleased/45702-fix-hashed-storage-repository-archive.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix repository archive generation when hashed storage is enabled
-merge_request: 19441
-author:
-type: fixed
diff --git a/changelogs/unreleased/45703-open-web-ide-file-tree.yml b/changelogs/unreleased/45703-open-web-ide-file-tree.yml
new file mode 100644
index 00000000000..abee9cad2d5
--- /dev/null
+++ b/changelogs/unreleased/45703-open-web-ide-file-tree.yml
@@ -0,0 +1,5 @@
+---
+title: Update WebIDE to show file in tree on load
+merge_request: 19887
+author:
+type: changed
diff --git a/changelogs/unreleased/45715-remove-modal-retry.yml b/changelogs/unreleased/45715-remove-modal-retry.yml
deleted file mode 100644
index 04f2ff5142e..00000000000
--- a/changelogs/unreleased/45715-remove-modal-retry.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove modalbox confirmation when retrying a pipeline
-merge_request: 18879
-author:
-type: changed
diff --git a/changelogs/unreleased/45820-add-xcode-link.yml b/changelogs/unreleased/45820-add-xcode-link.yml
deleted file mode 100644
index 9e61703ee10..00000000000
--- a/changelogs/unreleased/45820-add-xcode-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Open in Xcode link for xcode repositories
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/45821-avatar_api.yml b/changelogs/unreleased/45821-avatar_api.yml
deleted file mode 100644
index e16b28c36a2..00000000000
--- a/changelogs/unreleased/45821-avatar_api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Avatar API
-merge_request: 19121
-author: Imre Farkas
-type: added
diff --git a/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml b/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml
deleted file mode 100644
index 7c495cf4dc0..00000000000
--- a/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose readme url in Project API
-merge_request: 18960
-author: Imre Farkas
-type: changed
diff --git a/changelogs/unreleased/45850-close-mr-checkout-modal-on-escape.yml b/changelogs/unreleased/45850-close-mr-checkout-modal-on-escape.yml
deleted file mode 100644
index c3955c9d8b3..00000000000
--- a/changelogs/unreleased/45850-close-mr-checkout-modal-on-escape.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Closes MR check out branch modal with escape
-merge_request: Jacopo Beschi @jacopo-beschi
-author: 19050
-type: added
diff --git a/changelogs/unreleased/45933-webide-fade-uneditable-area.yml b/changelogs/unreleased/45933-webide-fade-uneditable-area.yml
new file mode 100644
index 00000000000..dfb186122e7
--- /dev/null
+++ b/changelogs/unreleased/45933-webide-fade-uneditable-area.yml
@@ -0,0 +1,5 @@
+---
+title: Fade uneditable area in Web IDE
+merge_request: 20008
+author:
+type: changed
diff --git a/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml
deleted file mode 100644
index b9e70bc5679..00000000000
--- a/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix unscrollable Markdown preview of WebIDE on Firefox
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/46010-add-index-to-runner-type.yml b/changelogs/unreleased/46010-add-index-to-runner-type.yml
deleted file mode 100644
index fb8340e37b2..00000000000
--- a/changelogs/unreleased/46010-add-index-to-runner-type.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add index on runner_type for ci_runners
-merge_request: 18897
-author:
-type: performance
diff --git a/changelogs/unreleased/46019-add-missing-migration.yml b/changelogs/unreleased/46019-add-missing-migration.yml
deleted file mode 100644
index e9c6c317de2..00000000000
--- a/changelogs/unreleased/46019-add-missing-migration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add missing migration for minimal Project build_timeout
-merge_request: 18775
-author:
-type: fixed
diff --git a/changelogs/unreleased/46075-automatically-provide-deploy-token-when-autodevops-is-enabled.yml b/changelogs/unreleased/46075-automatically-provide-deploy-token-when-autodevops-is-enabled.yml
deleted file mode 100644
index 6974be07716..00000000000
--- a/changelogs/unreleased/46075-automatically-provide-deploy-token-when-autodevops-is-enabled.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Automatize Deploy Token creation for Auto Devops
-merge_request: 19507
-author:
-type: added
diff --git a/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml b/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml
deleted file mode 100644
index 07f67251b24..00000000000
--- a/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Runner contacted at tooltip cache.
-merge_request: 18810
-author:
-type: fixed
diff --git a/changelogs/unreleased/46193-fix-big-estimate.yml b/changelogs/unreleased/46193-fix-big-estimate.yml
deleted file mode 100644
index d0da0c10033..00000000000
--- a/changelogs/unreleased/46193-fix-big-estimate.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes 500 error on /estimate BIG_VALUE
-merge_request: 18964
-author: Jacopo Beschi @jacopo-beschi
-type: fixed
diff --git a/changelogs/unreleased/46202-webide-file-states.yml b/changelogs/unreleased/46202-webide-file-states.yml
new file mode 100644
index 00000000000..8d697b643be
--- /dev/null
+++ b/changelogs/unreleased/46202-webide-file-states.yml
@@ -0,0 +1,5 @@
+---
+title: Update Web IDE file tree styles
+merge_request: 19969
+author:
+type: changed
diff --git a/changelogs/unreleased/46354-deprecate_gemnasium_service.yml b/changelogs/unreleased/46354-deprecate_gemnasium_service.yml
deleted file mode 100644
index c5ead45d883..00000000000
--- a/changelogs/unreleased/46354-deprecate_gemnasium_service.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Deprecate Gemnasium project service
-merge_request: 18954
-author:
-type: deprecated
diff --git a/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml b/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml
deleted file mode 100644
index e4255f11ecf..00000000000
--- a/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Does not log failed sign-in attempts when the database is in read-only mode
-merge_request: 18957
-author:
-type: fixed
diff --git a/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key-part-1.yml b/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key-part-1.yml
new file mode 100644
index 00000000000..d8c7d612c3d
--- /dev/null
+++ b/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key-part-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update new SSH key page to improve copy
+merge_request: 19994
+author:
+type: other
diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml
deleted file mode 100644
index 609968f3230..00000000000
--- a/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds keyboard shortcut `g e` for Environments on Project pages
-merge_request: 19002
-author:
-type: added
diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml
deleted file mode 100644
index 48e51b2615e..00000000000
--- a/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds keyboard shortcut `g k` for Kubernetes on Project pages
-merge_request: 19002
-author:
-type: added
diff --git a/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml
deleted file mode 100644
index 9a7cf0d6944..00000000000
--- a/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Changes keyboard shortcut of Activity feed to `g v`
-merge_request: 19002
-author:
-type: changed
diff --git a/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml
deleted file mode 100644
index f416e35030e..00000000000
--- a/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removes outdated `g t` shortcut for TODO in favor of `Shift+T`
-merge_request: 19002
-author:
-type: removed
diff --git a/changelogs/unreleased/46452-nomethoderror-undefined-method-previous_changes-for-nil-nilclass.yml b/changelogs/unreleased/46452-nomethoderror-undefined-method-previous_changes-for-nil-nilclass.yml
deleted file mode 100644
index 89dee65f5a8..00000000000
--- a/changelogs/unreleased/46452-nomethoderror-undefined-method-previous_changes-for-nil-nilclass.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Check for nil AutoDevOps when saving project CI/CD settings.
-merge_request: 19190
-author:
-type: fixed
diff --git a/changelogs/unreleased/46478-update-updated-at-on-mr.yml b/changelogs/unreleased/46478-update-updated-at-on-mr.yml
deleted file mode 100644
index c58b4fc8f84..00000000000
--- a/changelogs/unreleased/46478-update-updated-at-on-mr.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Updates updated_at on label changes
-merge_request: 19065
-author: Jacopo Beschi @jacopo-beschi
-type: fixed
diff --git a/changelogs/unreleased/46487-add-support-for-jupyter-in-gitlab-via-kubernetes.yml b/changelogs/unreleased/46487-add-support-for-jupyter-in-gitlab-via-kubernetes.yml
deleted file mode 100644
index 782ffd9a928..00000000000
--- a/changelogs/unreleased/46487-add-support-for-jupyter-in-gitlab-via-kubernetes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds JupyterHub to cluster applications
-merge_request: 19019
-author:
-type: added
diff --git a/changelogs/unreleased/46552-fixes-redundant-message-for-failure-reasons.yml b/changelogs/unreleased/46552-fixes-redundant-message-for-failure-reasons.yml
deleted file mode 100644
index 43427aaa242..00000000000
--- a/changelogs/unreleased/46552-fixes-redundant-message-for-failure-reasons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removes redundant script failure message from Job page
-merge_request: 19138
-author:
-type: changed
diff --git a/changelogs/unreleased/46571-webhooks-nil-password.yml b/changelogs/unreleased/46571-webhooks-nil-password.yml
new file mode 100644
index 00000000000..34c5f09478f
--- /dev/null
+++ b/changelogs/unreleased/46571-webhooks-nil-password.yml
@@ -0,0 +1,5 @@
+---
+title: Fix webhook error when password is not present
+merge_request: 19945
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/46585-gdpr-terms-acceptance.yml b/changelogs/unreleased/46585-gdpr-terms-acceptance.yml
deleted file mode 100644
index 84853846b0e..00000000000
--- a/changelogs/unreleased/46585-gdpr-terms-acceptance.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Add flash notice if user has already accepted terms and allow users to continue
- to root path
-merge_request: 19156
-author:
-type: changed
diff --git a/changelogs/unreleased/46648-timeout-searching-group-issues.yml b/changelogs/unreleased/46648-timeout-searching-group-issues.yml
deleted file mode 100644
index 54401edf5cc..00000000000
--- a/changelogs/unreleased/46648-timeout-searching-group-issues.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve performance of group issues filtering on GitLab.com
-merge_request: 19429
-author:
-type: performance
diff --git a/changelogs/unreleased/46783-removed-omniauth-provider-causing-invalid-application-setting.yml b/changelogs/unreleased/46783-removed-omniauth-provider-causing-invalid-application-setting.yml
new file mode 100644
index 00000000000..d5ecf5163d4
--- /dev/null
+++ b/changelogs/unreleased/46783-removed-omniauth-provider-causing-invalid-application-setting.yml
@@ -0,0 +1,5 @@
+---
+title: Ignore unknown OAuth sources in ApplicationSetting
+merge_request: 20129
+author:
+type: fixed
diff --git a/changelogs/unreleased/46831-remove-unused-bootstrap-component-css.yml b/changelogs/unreleased/46831-remove-unused-bootstrap-component-css.yml
new file mode 100644
index 00000000000..e0e2b481b69
--- /dev/null
+++ b/changelogs/unreleased/46831-remove-unused-bootstrap-component-css.yml
@@ -0,0 +1,5 @@
+---
+title: Removes unused bootstrap 4 scss files
+merge_request: 19423
+author:
+type: deprecated
diff --git a/changelogs/unreleased/46844-update-awesome_print-to-1-8-0.yml b/changelogs/unreleased/46844-update-awesome_print-to-1-8-0.yml
deleted file mode 100644
index e6dc9a6187b..00000000000
--- a/changelogs/unreleased/46844-update-awesome_print-to-1-8-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update awesome_print to 1.8.0
-merge_request: 19163
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/46845-update-email_spec-to-2-2-0.yml b/changelogs/unreleased/46845-update-email_spec-to-2-2-0.yml
deleted file mode 100644
index bf501340769..00000000000
--- a/changelogs/unreleased/46845-update-email_spec-to-2-2-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update email_spec to 2.2.0
-merge_request: 19164
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml b/changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml
deleted file mode 100644
index 3707ad74b8f..00000000000
--- a/changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update redis-namespace to 1.6.0
-merge_request: 19166
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/46849-update-rdoc-to-6-0-4.yml b/changelogs/unreleased/46849-update-rdoc-to-6-0-4.yml
deleted file mode 100644
index cf0436df1a7..00000000000
--- a/changelogs/unreleased/46849-update-rdoc-to-6-0-4.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update rdoc to 6.0.4
-merge_request: 19167
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/46903-osw-fix-permitted-params-filtering-on-merge-scheduling.yml b/changelogs/unreleased/46903-osw-fix-permitted-params-filtering-on-merge-scheduling.yml
deleted file mode 100644
index b3c8c8e4045..00000000000
--- a/changelogs/unreleased/46903-osw-fix-permitted-params-filtering-on-merge-scheduling.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust permitted params filtering on merge scheduling
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/46922-hashed-storage-single-project.yml b/changelogs/unreleased/46922-hashed-storage-single-project.yml
deleted file mode 100644
index c293238a5a4..00000000000
--- a/changelogs/unreleased/46922-hashed-storage-single-project.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Hashed Storage: migration rake task now can be executed to specific project'
-merge_request: 19268
-author:
-type: changed
diff --git a/changelogs/unreleased/46999-line-profiling-modal-width.yml b/changelogs/unreleased/46999-line-profiling-modal-width.yml
deleted file mode 100644
index 130f50d1ec0..00000000000
--- a/changelogs/unreleased/46999-line-profiling-modal-width.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix UI broken in line profiling modal due to Bootstrap 4
-merge_request: 19253
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/47046-use-sortable-from-npm.yml b/changelogs/unreleased/47046-use-sortable-from-npm.yml
deleted file mode 100644
index 35bd6f49e4b..00000000000
--- a/changelogs/unreleased/47046-use-sortable-from-npm.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use NPM provided version of SortableJS
-merge_request: 19274
-author:
-type: performance
diff --git a/changelogs/unreleased/47113-modal-header-styling-is-broken.yml b/changelogs/unreleased/47113-modal-header-styling-is-broken.yml
deleted file mode 100644
index 1c78e5d4211..00000000000
--- a/changelogs/unreleased/47113-modal-header-styling-is-broken.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes the styling on the modal headers
-merge_request: 19312
-author: samdbeckham
-type: fixed
diff --git a/changelogs/unreleased/47182-use-the-default-strings-of-timeago-js.yml b/changelogs/unreleased/47182-use-the-default-strings-of-timeago-js.yml
deleted file mode 100644
index 010b1db5aac..00000000000
--- a/changelogs/unreleased/47182-use-the-default-strings-of-timeago-js.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use the default strings of timeago.js for timeago
-merge_request: 19350
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/47183-update-selenium-webdriver-to-3-12-0.yml b/changelogs/unreleased/47183-update-selenium-webdriver-to-3-12-0.yml
deleted file mode 100644
index b0d51d810f2..00000000000
--- a/changelogs/unreleased/47183-update-selenium-webdriver-to-3-12-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update selenium-webdriver to 3.12.0
-merge_request: 19351
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/47189-github_import_visibility.yml b/changelogs/unreleased/47189-github_import_visibility.yml
deleted file mode 100644
index a2a727a3227..00000000000
--- a/changelogs/unreleased/47189-github_import_visibility.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Use Github repo visibility during import while respecting restricted visibility
- levels
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/47208-human-import-status-name-not-working.yml b/changelogs/unreleased/47208-human-import-status-name-not-working.yml
deleted file mode 100644
index e1f603f988e..00000000000
--- a/changelogs/unreleased/47208-human-import-status-name-not-working.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Showing project import_status in a humanized form no longer gives an error
-merge_request: 19470
-author:
-type: fixed
diff --git a/changelogs/unreleased/47221-explain-what-groups-are-in-the-new-group-page.yml b/changelogs/unreleased/47221-explain-what-groups-are-in-the-new-group-page.yml
new file mode 100644
index 00000000000..94c58a3863a
--- /dev/null
+++ b/changelogs/unreleased/47221-explain-what-groups-are-in-the-new-group-page.yml
@@ -0,0 +1,5 @@
+---
+title: Update new group page to better explain what groups are
+merge_request: 19991
+author:
+type: other
diff --git a/changelogs/unreleased/47274-help-users-find-our-contributing-page.yml b/changelogs/unreleased/47274-help-users-find-our-contributing-page.yml
new file mode 100644
index 00000000000..ed13c917a2e
--- /dev/null
+++ b/changelogs/unreleased/47274-help-users-find-our-contributing-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add a link to the contributing page in the user dropdown
+merge_request: 19708
+author:
+type: added
diff --git a/changelogs/unreleased/47408-migrateuploadsworker-is-doing-n-1-queries-on-migration.yml b/changelogs/unreleased/47408-migrateuploadsworker-is-doing-n-1-queries-on-migration.yml
deleted file mode 100644
index c0df82f35f1..00000000000
--- a/changelogs/unreleased/47408-migrateuploadsworker-is-doing-n-1-queries-on-migration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Optimize the upload migration proces
-merge_request: 15947
-author:
-type: fixed
diff --git a/changelogs/unreleased/47513-upload-migration-lease-key-is-incorrect-for-non-mounted-uploaders.yml b/changelogs/unreleased/47513-upload-migration-lease-key-is-incorrect-for-non-mounted-uploaders.yml
deleted file mode 100644
index 010c4e9bce7..00000000000
--- a/changelogs/unreleased/47513-upload-migration-lease-key-is-incorrect-for-non-mounted-uploaders.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use upload ID for creating lease key for file uploaders.
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/47604-avatars-and-system-icons-for-mobile.yml b/changelogs/unreleased/47604-avatars-and-system-icons-for-mobile.yml
deleted file mode 100644
index ff66385375f..00000000000
--- a/changelogs/unreleased/47604-avatars-and-system-icons-for-mobile.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make avatars/icons hidden on mobile
-merge_request: 19585
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/47646-ui-glitch.yml b/changelogs/unreleased/47646-ui-glitch.yml
deleted file mode 100644
index 384df4e2cc9..00000000000
--- a/changelogs/unreleased/47646-ui-glitch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Line height fixed
-merge_request:
-author: Murat Dogan
-type: fixed
diff --git a/changelogs/unreleased/47672-set_inline_content_type_for_ics.yml b/changelogs/unreleased/47672-set_inline_content_type_for_ics.yml
deleted file mode 100644
index 4bc883d41bd..00000000000
--- a/changelogs/unreleased/47672-set_inline_content_type_for_ics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Render calendar feed inline when accessed from GitLab
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/47679-fix-failed-jobs-tab-ie11.yml b/changelogs/unreleased/47679-fix-failed-jobs-tab-ie11.yml
deleted file mode 100644
index 48f3bc87eee..00000000000
--- a/changelogs/unreleased/47679-fix-failed-jobs-tab-ie11.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix overflowing Failed Jobs table in sm viewports on IE11
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/47794-environment-scope-cluster-page.yml b/changelogs/unreleased/47794-environment-scope-cluster-page.yml
new file mode 100644
index 00000000000..75eb7ec209c
--- /dev/null
+++ b/changelogs/unreleased/47794-environment-scope-cluster-page.yml
@@ -0,0 +1,6 @@
+---
+title: Change environment scope text depending on number of project clusters. Update
+ form to only include form-groups
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/47871-new-mr-tab-active-state.yml b/changelogs/unreleased/47871-new-mr-tab-active-state.yml
deleted file mode 100644
index c3fc8672d82..00000000000
--- a/changelogs/unreleased/47871-new-mr-tab-active-state.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix active tab highlight when creating new merge request
-merge_request: 19781
-author: Jan Beckmann
-type: fixed
diff --git a/changelogs/unreleased/48126-fix-prometheus-installation.yml b/changelogs/unreleased/48126-fix-prometheus-installation.yml
deleted file mode 100644
index e6ab9c46fbf..00000000000
--- a/changelogs/unreleased/48126-fix-prometheus-installation.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Specify chart version when installing applications on Clusters
-merge_request: 20010
-author:
-type: fixed
diff --git a/changelogs/unreleased/48378-avatar-upload.yml b/changelogs/unreleased/48378-avatar-upload.yml
new file mode 100644
index 00000000000..1e359ee72d5
--- /dev/null
+++ b/changelogs/unreleased/48378-avatar-upload.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes issue with uploading same image to Profile Avatar twice
+merge_request: 20161
+author: Chirag Bhatia
+type: fixed
diff --git a/changelogs/unreleased/ab-35364-throttle-updates-last-repository-at.yml b/changelogs/unreleased/ab-35364-throttle-updates-last-repository-at.yml
deleted file mode 100644
index 8e468233637..00000000000
--- a/changelogs/unreleased/ab-35364-throttle-updates-last-repository-at.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Throttle updates to Project#last_repository_updated_at.
-merge_request: 19183
-author:
-type: performance
diff --git a/changelogs/unreleased/ab-43706-composite-primary-keys.yml b/changelogs/unreleased/ab-43706-composite-primary-keys.yml
deleted file mode 100644
index b17050a64c8..00000000000
--- a/changelogs/unreleased/ab-43706-composite-primary-keys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add NOT NULL constraints to project_authorizations.
-merge_request: 18980
-author:
-type: other
diff --git a/changelogs/unreleased/ab-45389-remove-double-checked-internal-id-generation.yml b/changelogs/unreleased/ab-45389-remove-double-checked-internal-id-generation.yml
deleted file mode 100644
index d87604de0f8..00000000000
--- a/changelogs/unreleased/ab-45389-remove-double-checked-internal-id-generation.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove double-checked internal id generation.
-merge_request: 19181
-author:
-type: performance
diff --git a/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml b/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml
deleted file mode 100644
index 88ef62ebc0e..00000000000
--- a/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Increase text limit for GPG keys (mysql only).
-merge_request: 19069
-author:
-type: other
diff --git a/changelogs/unreleased/add-artifacts_expire_at-to-api.yml b/changelogs/unreleased/add-artifacts_expire_at-to-api.yml
deleted file mode 100644
index 7fe0d8b5720..00000000000
--- a/changelogs/unreleased/add-artifacts_expire_at-to-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose artifacts_expire_at field for job entity in api
-merge_request: 18872
-author: Semyon Pupkov
-type: added
diff --git a/changelogs/unreleased/add-background-migration-to-fill-file-store.yml b/changelogs/unreleased/add-background-migration-to-fill-file-store.yml
deleted file mode 100644
index ab6bde86fd4..00000000000
--- a/changelogs/unreleased/add-background-migration-to-fill-file-store.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add backgound migration for filling nullfied file_store columns
-merge_request: 18557
-author:
-type: performance
diff --git a/changelogs/unreleased/add-background-migrations-for-not-archived-traces.yml b/changelogs/unreleased/add-background-migrations-for-not-archived-traces.yml
deleted file mode 100644
index b1b23c477df..00000000000
--- a/changelogs/unreleased/add-background-migrations-for-not-archived-traces.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add background migrations for archiving legacy job traces
-merge_request: 19194
-author:
-type: performance
diff --git a/changelogs/unreleased/add-missing-index-for-deployments.yml b/changelogs/unreleased/add-missing-index-for-deployments.yml
new file mode 100644
index 00000000000..7863c0ee039
--- /dev/null
+++ b/changelogs/unreleased/add-missing-index-for-deployments.yml
@@ -0,0 +1,5 @@
+---
+title: Add index on deployable_type/id for deployments
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/add-moneky-patch-for-using-stream-upload-with-carrierwave.yml b/changelogs/unreleased/add-moneky-patch-for-using-stream-upload-with-carrierwave.yml
deleted file mode 100644
index 22a2369a264..00000000000
--- a/changelogs/unreleased/add-moneky-patch-for-using-stream-upload-with-carrierwave.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix CarrierWave reads local files into memoery when migrates to ObjectStorage
-merge_request: 19102
-author:
-type: performance
diff --git a/changelogs/unreleased/add-new-arg-to-git-rev-list-call.yml b/changelogs/unreleased/add-new-arg-to-git-rev-list-call.yml
deleted file mode 100644
index 86680b6b117..00000000000
--- a/changelogs/unreleased/add-new-arg-to-git-rev-list-call.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve performance of LFS integrity check
-merge_request: 19494
-author:
-type: performance
diff --git a/changelogs/unreleased/bjk-48176_ruby_gc.yml b/changelogs/unreleased/bjk-48176_ruby_gc.yml
new file mode 100644
index 00000000000..45c6338df81
--- /dev/null
+++ b/changelogs/unreleased/bjk-48176_ruby_gc.yml
@@ -0,0 +1,5 @@
+---
+title: Cleanup Prometheus ruby metrics
+merge_request: 20039
+author: Ben Kochie
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-fix-protect-from-forgery-in-application-controller.yml b/changelogs/unreleased/blackst0ne-fix-protect-from-forgery-in-application-controller.yml
new file mode 100644
index 00000000000..da75ea8b09e
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-fix-protect-from-forgery-in-application-controller.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Force the callback run first"
+merge_request: 20055
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-fix-data-store-spec.yml b/changelogs/unreleased/blackst0ne-rails5-fix-data-store-spec.yml
new file mode 100644
index 00000000000..403c3764321
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-fix-data-store-spec.yml
@@ -0,0 +1,5 @@
+---
+title: '[Rails5] Fix "-1 is not a valid data_store"'
+merge_request: 19917
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-set-request-format-in--commits-controller.yml b/changelogs/unreleased/blackst0ne-rails5-set-request-format-in--commits-controller.yml
new file mode 100644
index 00000000000..3f8f8fd5d66
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-set-request-format-in--commits-controller.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Set request.format in commits_controller"
+merge_request: 20023
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-remove-spinach.yml b/changelogs/unreleased/blackst0ne-remove-spinach.yml
deleted file mode 100644
index 104da257bad..00000000000
--- a/changelogs/unreleased/blackst0ne-remove-spinach.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove Spinach
-merge_request: 18869
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml
deleted file mode 100644
index 7014de4ece7..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Replace the `project/deploy_keys.feature` spinach test with an rspec analog'
-merge_request: 18796
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml
deleted file mode 100644
index 7802391ec64..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Replace the `project/ff_merge_requests.feature` spinach test with an rspec analog'
-merge_request: 18800
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml
deleted file mode 100644
index 2ac43490c26..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Replace the `project/forked_merge_requests.feature` spinach test with an rspec analog'
-merge_request: 18867
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml
deleted file mode 100644
index 968a937ca5a..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Replace the `project/issues/references.feature` spinach test with an rspec analog'
-merge_request: 18769
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml
deleted file mode 100644
index c0ba984bfdc..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Replace the `project/merge_requests/references.feature` spinach test with an rspec analog'
-merge_request: 18794
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-squash-and-merge-in-gitlab-core-ce.yml b/changelogs/unreleased/blackst0ne-squash-and-merge-in-gitlab-core-ce.yml
deleted file mode 100644
index e603c835b5e..00000000000
--- a/changelogs/unreleased/blackst0ne-squash-and-merge-in-gitlab-core-ce.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add `Squash and merge` to GitLab Core (CE)
-merge_request: 18956
-author: "@blackst0ne"
-type: added
diff --git a/changelogs/unreleased/bootstrap-changelog.yml b/changelogs/unreleased/bootstrap-changelog.yml
deleted file mode 100644
index fd58447769d..00000000000
--- a/changelogs/unreleased/bootstrap-changelog.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade GitLab from Bootstrap 3 to 4
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/bump-kubeclient-version-3-1-0.yml b/changelogs/unreleased/bump-kubeclient-version-3-1-0.yml
deleted file mode 100644
index 24f240410b0..00000000000
--- a/changelogs/unreleased/bump-kubeclient-version-3-1-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Updates the version of kubeclient from 3.0 to 3.1.0
-merge_request: 19199
-author:
-type: other
diff --git a/changelogs/unreleased/bvl-add-username-to-terms-message.yml b/changelogs/unreleased/bvl-add-username-to-terms-message.yml
deleted file mode 100644
index b95d87e9265..00000000000
--- a/changelogs/unreleased/bvl-add-username-to-terms-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add username to terms message in git and API calls
-merge_request: 19126
-author:
-type: changed
diff --git a/changelogs/unreleased/bvl-bump-gitlab-shell-7-1-3.yml b/changelogs/unreleased/bvl-bump-gitlab-shell-7-1-3.yml
deleted file mode 100644
index 76bb25bc7d7..00000000000
--- a/changelogs/unreleased/bvl-bump-gitlab-shell-7-1-3.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Include username in output when testing SSH to GitLab
-merge_request: 19358
-author:
-type: other
diff --git a/changelogs/unreleased/bvl-graphql-start-34754.yml b/changelogs/unreleased/bvl-graphql-start-34754.yml
deleted file mode 100644
index a31f46d3a61..00000000000
--- a/changelogs/unreleased/bvl-graphql-start-34754.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Setup graphql with initial project & merge request query
-merge_request: 19008
-author:
-type: added
diff --git a/changelogs/unreleased/bvl-terms-on-registration.yml b/changelogs/unreleased/bvl-terms-on-registration.yml
deleted file mode 100644
index 3e6e499dd02..00000000000
--- a/changelogs/unreleased/bvl-terms-on-registration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Users can accept terms during registration
-merge_request: 19583
-author:
-type: other
diff --git a/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml
deleted file mode 100644
index a0d787e570e..00000000000
--- a/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-title: Add anchor for incoming email regex
-merge_request: !18917
-type: added
diff --git a/changelogs/unreleased/collapsed-contextual-nav-update.yml b/changelogs/unreleased/collapsed-contextual-nav-update.yml
deleted file mode 100644
index 31a32a9e1e9..00000000000
--- a/changelogs/unreleased/collapsed-contextual-nav-update.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Renamed 'Overview' to 'Project' in collapsed contextual navigation at a project
- level
-merge_request: 18996
-author: Constance Okoghenun
-type: fixed
diff --git a/changelogs/unreleased/commit-branch-tag-icon-update.yml b/changelogs/unreleased/commit-branch-tag-icon-update.yml
deleted file mode 100644
index 136b7cbf0f4..00000000000
--- a/changelogs/unreleased/commit-branch-tag-icon-update.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Updated icons for branch and tag names in commit details
-merge_request: 18953
-author: Constance Okoghenun
-type: changed
diff --git a/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml b/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml
deleted file mode 100644
index f32c70cf884..00000000000
--- a/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Forbid to patch traces for finished jobs
-merge_request: 18969
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-api-projects-members-preload.yml b/changelogs/unreleased/dm-api-projects-members-preload.yml
deleted file mode 100644
index e04e7c37d13..00000000000
--- a/changelogs/unreleased/dm-api-projects-members-preload.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Only preload member records for the relevant projects/groups/user in projects
- API
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/dm-label-reference-period.yml b/changelogs/unreleased/dm-label-reference-period.yml
new file mode 100644
index 00000000000..9fdd590641d
--- /dev/null
+++ b/changelogs/unreleased/dm-label-reference-period.yml
@@ -0,0 +1,5 @@
+---
+title: Properly detect label reference if followed by period or question mark
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/docs-42067-document-runner-registration-api.yml b/changelogs/unreleased/docs-42067-document-runner-registration-api.yml
deleted file mode 100644
index 6b507174044..00000000000
--- a/changelogs/unreleased/docs-42067-document-runner-registration-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expand documentation for Runners API
-merge_request: 16484
-author:
-type: other
diff --git a/changelogs/unreleased/dz-add-2fa-filter.yml b/changelogs/unreleased/dz-add-2fa-filter.yml
deleted file mode 100644
index 82d501d6604..00000000000
--- a/changelogs/unreleased/dz-add-2fa-filter.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add 2FA filter to the group members page
-merge_request: 18483
-author:
-type: changed
diff --git a/changelogs/unreleased/dz-redesign-group-settings-page.yml b/changelogs/unreleased/dz-redesign-group-settings-page.yml
deleted file mode 100644
index 4a8dfbb61dc..00000000000
--- a/changelogs/unreleased/dz-redesign-group-settings-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Redesign group settings page into expandable sections
-merge_request: 19184
-author:
-type: changed
diff --git a/changelogs/unreleased/existing-gcp-accounts.yml b/changelogs/unreleased/existing-gcp-accounts.yml
new file mode 100644
index 00000000000..ce396c70b4a
--- /dev/null
+++ b/changelogs/unreleased/existing-gcp-accounts.yml
@@ -0,0 +1,5 @@
+---
+title: Add back copy for existing gcp accounts within offer banner
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/feature-customizable-favicon.yml b/changelogs/unreleased/feature-customizable-favicon.yml
deleted file mode 100644
index 0e5afc17c9e..00000000000
--- a/changelogs/unreleased/feature-customizable-favicon.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow changing the default favicon to a custom icon.
-merge_request: 14497
-author: Alexis Reigel
-type: added
diff --git a/changelogs/unreleased/feature-expose-runner-ip-to-api.yml b/changelogs/unreleased/feature-expose-runner-ip-to-api.yml
deleted file mode 100644
index e755cf5f2d4..00000000000
--- a/changelogs/unreleased/feature-expose-runner-ip-to-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose runner ip address to runners API
-merge_request: 18799
-author: Lars Greiss
-type: changed
diff --git a/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml
deleted file mode 100644
index d77c5b42497..00000000000
--- a/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for variables expression pattern matching syntax
-merge_request: 18902
-author:
-type: added
diff --git a/changelogs/unreleased/fix-assignee-name-wrap.yml b/changelogs/unreleased/fix-assignee-name-wrap.yml
deleted file mode 100644
index 2407288785f..00000000000
--- a/changelogs/unreleased/fix-assignee-name-wrap.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Wrapping problem on the issues page has been fixed
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-avatars-n-plus-one.yml b/changelogs/unreleased/fix-avatars-n-plus-one.yml
deleted file mode 100644
index c5b42071f2b..00000000000
--- a/changelogs/unreleased/fix-avatars-n-plus-one.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix an N+1 when loading user avatars
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/fix-bitbucket_import_anonymous.yml b/changelogs/unreleased/fix-bitbucket_import_anonymous.yml
deleted file mode 100644
index 6e214b3c957..00000000000
--- a/changelogs/unreleased/fix-bitbucket_import_anonymous.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Import bitbucket issues that are reported by an anonymous user
-merge_request: 18199
-author: bartl
-type: fixed
diff --git a/changelogs/unreleased/fix-boards-issue-highlight.yml b/changelogs/unreleased/fix-boards-issue-highlight.yml
new file mode 100644
index 00000000000..0cc3faa81ca
--- /dev/null
+++ b/changelogs/unreleased/fix-boards-issue-highlight.yml
@@ -0,0 +1,5 @@
+---
+title: Fix boards issue highlight
+merge_request: 20063
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/fix-devops-remove-beta.yml b/changelogs/unreleased/fix-devops-remove-beta.yml
deleted file mode 100644
index 326003eb956..00000000000
--- a/changelogs/unreleased/fix-devops-remove-beta.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removed "(Beta)" from "Auto DevOps" messages
-merge_request: 18759
-author:
-type: changed
diff --git a/changelogs/unreleased/fix-favicon-cross-origin.yml b/changelogs/unreleased/fix-favicon-cross-origin.yml
deleted file mode 100644
index 3317781e222..00000000000
--- a/changelogs/unreleased/fix-favicon-cross-origin.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Serve favicon image always from the main GitLab domain to avoid issues with CORS
-merge_request: 19810
-author: Alexis Reigel
-type: fixed
diff --git a/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml b/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml
deleted file mode 100644
index 92426832f30..00000000000
--- a/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Exclude CI_PIPELINE_ID from variables supported in dynamic environment name
-merge_request: 19032
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml
deleted file mode 100644
index c2a788d6ad0..00000000000
--- a/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Do not allow to trigger manual actions that were skipped
-merge_request: 18985
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-http-proxy.yml b/changelogs/unreleased/fix-http-proxy.yml
deleted file mode 100644
index 806b7d0a38c..00000000000
--- a/changelogs/unreleased/fix-http-proxy.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed HTTP_PROXY environment not honored when reading remote traces.
-merge_request: 19282
-author: NLR
-type: fixed
diff --git a/changelogs/unreleased/fix-missing-timeout.yml b/changelogs/unreleased/fix-missing-timeout.yml
deleted file mode 100644
index e0a61eb866c..00000000000
--- a/changelogs/unreleased/fix-missing-timeout.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Missing timeout value in object storage pre-authorization
-merge_request: 19201
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-nbsp-after-sign-in-with-google.yml b/changelogs/unreleased/fix-nbsp-after-sign-in-with-google.yml
deleted file mode 100644
index 73b478eff3e..00000000000
--- a/changelogs/unreleased/fix-nbsp-after-sign-in-with-google.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix &nbsp; after sign-in with Google button
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-reactive-cache-retry-rate.yml b/changelogs/unreleased/fix-reactive-cache-retry-rate.yml
deleted file mode 100644
index 044e7fe39c0..00000000000
--- a/changelogs/unreleased/fix-reactive-cache-retry-rate.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update commit status from external CI services less aggressively
-merge_request: 18802
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-registry-created-at-tooltip.yml b/changelogs/unreleased/fix-registry-created-at-tooltip.yml
deleted file mode 100644
index 911b3b10fd4..00000000000
--- a/changelogs/unreleased/fix-registry-created-at-tooltip.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Add missing tooltip to creation date on container registry overview'
-merge_request: 18767
-author: Lars Greiss
-type: fixed
diff --git a/changelogs/unreleased/fix-shorcut-modal.yml b/changelogs/unreleased/fix-shorcut-modal.yml
deleted file mode 100644
index 796a1523a61..00000000000
--- a/changelogs/unreleased/fix-shorcut-modal.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix modal width of shorcuts help page
-merge_request: 18766
-author: Lars Greiss
-type: fixed
diff --git a/changelogs/unreleased/fix-unverified-hover-state.yml b/changelogs/unreleased/fix-unverified-hover-state.yml
deleted file mode 100644
index 003138f9821..00000000000
--- a/changelogs/unreleased/fix-unverified-hover-state.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Unverified hover state color changed to black
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-34526-enabling-wiki-search-by-title.yml b/changelogs/unreleased/fj-34526-enabling-wiki-search-by-title.yml
deleted file mode 100644
index 2ae2cf8a23e..00000000000
--- a/changelogs/unreleased/fj-34526-enabling-wiki-search-by-title.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added ability to search by wiki titles
-merge_request: 19112
-author:
-type: added
diff --git a/changelogs/unreleased/fj-36819-remove-v3-api.yml b/changelogs/unreleased/fj-36819-remove-v3-api.yml
deleted file mode 100644
index e5355252458..00000000000
--- a/changelogs/unreleased/fj-36819-remove-v3-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removed API v3 from the codebase
-merge_request: 18970
-author:
-type: removed
diff --git a/changelogs/unreleased/fj-40401-support-import-lfs-objects.yml b/changelogs/unreleased/fj-40401-support-import-lfs-objects.yml
deleted file mode 100644
index a8abdd943ba..00000000000
--- a/changelogs/unreleased/fj-40401-support-import-lfs-objects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added support for LFS Download in the importing process
-merge_request: 18871
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-45059-add-validation-to-webhook.yml b/changelogs/unreleased/fj-45059-add-validation-to-webhook.yml
deleted file mode 100644
index e9350cc7e7e..00000000000
--- a/changelogs/unreleased/fj-45059-add-validation-to-webhook.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Refactoring UrlValidators to include url blocking
-merge_request: 18686
-author:
-type: changed
diff --git a/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml b/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml
deleted file mode 100644
index bd4e5a43352..00000000000
--- a/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed badge api endpoint route when relative url is set
-merge_request: 19004
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml b/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml
deleted file mode 100644
index 16b0ee06898..00000000000
--- a/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed bug where generated api urls didn't add the base url if set
-merge_request: 19003
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-relax-url-validator-rules-for-user.yml b/changelogs/unreleased/fj-relax-url-validator-rules-for-user.yml
deleted file mode 100644
index 4c72e4c5c1c..00000000000
--- a/changelogs/unreleased/fj-relax-url-validator-rules-for-user.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Avoid checking the user format in every url validation
-merge_request: 19575
-author:
-type: changed
diff --git a/changelogs/unreleased/fj-restore-users-v3-endpoint.yml b/changelogs/unreleased/fj-restore-users-v3-endpoint.yml
deleted file mode 100644
index c5f952dfa88..00000000000
--- a/changelogs/unreleased/fj-restore-users-v3-endpoint.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Restore API v3 user endpoint
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/gh-importer-transactions.yml b/changelogs/unreleased/gh-importer-transactions.yml
deleted file mode 100644
index 1489d60a3fb..00000000000
--- a/changelogs/unreleased/gh-importer-transactions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move PR IO operations out of a transaction
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/groups-controller-show-performance.yml b/changelogs/unreleased/groups-controller-show-performance.yml
deleted file mode 100644
index bab54cc455e..00000000000
--- a/changelogs/unreleased/groups-controller-show-performance.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve performance of GroupsController#show
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml b/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml
deleted file mode 100644
index 9efef2c6839..00000000000
--- a/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hide merge request option in IDE when disabled
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/ide-url-util-relative-url-fix.yml b/changelogs/unreleased/ide-url-util-relative-url-fix.yml
deleted file mode 100644
index 9f0f4a0f7be..00000000000
--- a/changelogs/unreleased/ide-url-util-relative-url-fix.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fixes Web IDE button on merge requests when GitLab is installed with relative
- URL
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/ignore-writing-trace-if-it-already-archived.yml b/changelogs/unreleased/ignore-writing-trace-if-it-already-archived.yml
deleted file mode 100644
index 5c342e2a131..00000000000
--- a/changelogs/unreleased/ignore-writing-trace-if-it-already-archived.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Disallow updating job status if the job is not running
-merge_request: 19101
-author:
-type: fixed
diff --git a/changelogs/unreleased/introduce-job-keep-alive-api-endpoint.yml b/changelogs/unreleased/introduce-job-keep-alive-api-endpoint.yml
deleted file mode 100644
index 0789fc34f27..00000000000
--- a/changelogs/unreleased/introduce-job-keep-alive-api-endpoint.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make CI job update entrypoint to work as keep-alive endpoint
-merge_request: 19543
-author:
-type: changed
diff --git a/changelogs/unreleased/issue-25256.yml b/changelogs/unreleased/issue-25256.yml
deleted file mode 100644
index e981b5f9a8f..00000000000
--- a/changelogs/unreleased/issue-25256.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't trim incoming emails that create new issues
-merge_request:
-author: Cameron Crockett
-type: fixed
diff --git a/changelogs/unreleased/issue_38418.yml b/changelogs/unreleased/issue_38418.yml
deleted file mode 100644
index 79452b27e4b..00000000000
--- a/changelogs/unreleased/issue_38418.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix issue count on sidebar
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/issue_44230.yml b/changelogs/unreleased/issue_44230.yml
deleted file mode 100644
index 2c6dba6c0fb..00000000000
--- a/changelogs/unreleased/issue_44230.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Apply notification settings level of groups to all child objects
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/issue_45082.yml b/changelogs/unreleased/issue_45082.yml
deleted file mode 100644
index b916a36c17b..00000000000
--- a/changelogs/unreleased/issue_45082.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add merge requests list endpoint for groups
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/issue_47729.yml b/changelogs/unreleased/issue_47729.yml
new file mode 100644
index 00000000000..e27972af114
--- /dev/null
+++ b/changelogs/unreleased/issue_47729.yml
@@ -0,0 +1,5 @@
+---
+title: Fix refreshing cache keys for open issues count
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jivl-add-dot-system-notes.yml b/changelogs/unreleased/jivl-add-dot-system-notes.yml
deleted file mode 100644
index 2246bab1464..00000000000
--- a/changelogs/unreleased/jivl-add-dot-system-notes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add dot to separate system notes content
-merge_request: 18864
-author:
-type: changed
diff --git a/changelogs/unreleased/jivl-smarter-system-notes.yml b/changelogs/unreleased/jivl-smarter-system-notes.yml
deleted file mode 100644
index e640981de9a..00000000000
--- a/changelogs/unreleased/jivl-smarter-system-notes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for smarter system notes
-merge_request: 17164
-author:
-type: changed
diff --git a/changelogs/unreleased/jprovazn-fix-resolvable.yml b/changelogs/unreleased/jprovazn-fix-resolvable.yml
deleted file mode 100644
index e17c409e290..00000000000
--- a/changelogs/unreleased/jprovazn-fix-resolvable.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix resolvable check if note's commit could not be found.
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jprovazn-null-byte.yml b/changelogs/unreleased/jprovazn-null-byte.yml
deleted file mode 100644
index 4c4760ac412..00000000000
--- a/changelogs/unreleased/jprovazn-null-byte.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix filename matching when processing file or blob search results
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jprovazn-pipeline-policy.yml b/changelogs/unreleased/jprovazn-pipeline-policy.yml
deleted file mode 100644
index 2997c6c8667..00000000000
--- a/changelogs/unreleased/jprovazn-pipeline-policy.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Allow maintainers to retry pipelines on forked projects (if allowed in merge
- request)
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jprovazn-remote-upload-destroy.yml b/changelogs/unreleased/jprovazn-remote-upload-destroy.yml
deleted file mode 100644
index 22e55920fa3..00000000000
--- a/changelogs/unreleased/jprovazn-remote-upload-destroy.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix deletion of Object Store uploads
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jprovazn-uploader-migration.yml b/changelogs/unreleased/jprovazn-uploader-migration.yml
deleted file mode 100644
index 1db67e9ace2..00000000000
--- a/changelogs/unreleased/jprovazn-uploader-migration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Migrate any remaining jobs from deprecated `object_storage_upload` queue.
-merge_request:
-author:
-type: deprecated
diff --git a/changelogs/unreleased/jr-48133-web-ide-commit-ellipsis.yml b/changelogs/unreleased/jr-48133-web-ide-commit-ellipsis.yml
new file mode 100644
index 00000000000..ac58eaccaaf
--- /dev/null
+++ b/changelogs/unreleased/jr-48133-web-ide-commit-ellipsis.yml
@@ -0,0 +1,5 @@
+---
+title: Add ellispsis to web ide commit button
+merge_request: 20030
+author:
+type: other
diff --git a/changelogs/unreleased/jr-web-ide-shortcuts.yml b/changelogs/unreleased/jr-web-ide-shortcuts.yml
deleted file mode 100644
index a895eab432a..00000000000
--- a/changelogs/unreleased/jr-web-ide-shortcuts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add shortcuts to Web IDE docs and modal
-merge_request: 19044
-author:
-type: changed
diff --git a/changelogs/unreleased/live-trace-v2-persist-data.yml b/changelogs/unreleased/live-trace-v2-persist-data.yml
deleted file mode 100644
index 3ac47b04218..00000000000
--- a/changelogs/unreleased/live-trace-v2-persist-data.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add a cronworker to rescue stale live traces
-merge_request: 18680
-author:
-type: performance
diff --git a/changelogs/unreleased/mattermost-api-v4.yml b/changelogs/unreleased/mattermost-api-v4.yml
deleted file mode 100644
index 8c5033f2a0c..00000000000
--- a/changelogs/unreleased/mattermost-api-v4.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Updated Mattermost integration to use API v4 and only allow creation of Mattermost slash commands in the current user's teams
-merge_request: 19043
-author: Harrison Healey
-type: changed
diff --git a/changelogs/unreleased/migrate-restore-repo-to-gitaly.yml b/changelogs/unreleased/migrate-restore-repo-to-gitaly.yml
deleted file mode 100644
index 59f375de20e..00000000000
--- a/changelogs/unreleased/migrate-restore-repo-to-gitaly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support restoring repositories into gitaly
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/mk-rake-task-verify-remote-files.yml b/changelogs/unreleased/mk-rake-task-verify-remote-files.yml
deleted file mode 100644
index 772aa11d89b..00000000000
--- a/changelogs/unreleased/mk-rake-task-verify-remote-files.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for verifying remote uploads, artifacts, and LFS objects in check rake tasks
-merge_request: 19501
-author:
-type: added
diff --git a/changelogs/unreleased/move-boards-modal-empty-state-vue-component.yml b/changelogs/unreleased/move-boards-modal-empty-state-vue-component.yml
new file mode 100644
index 00000000000..54a61d7c914
--- /dev/null
+++ b/changelogs/unreleased/move-boards-modal-empty-state-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move boards modal EmptyState vue component
+merge_request: 20068
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/move-disussion-actions-to-the-right.yml b/changelogs/unreleased/move-disussion-actions-to-the-right.yml
deleted file mode 100644
index b79c6f36585..00000000000
--- a/changelogs/unreleased/move-disussion-actions-to-the-right.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move discussion actions to the right for small viewports
-merge_request: 18476
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/mr-conflict-notification.yml b/changelogs/unreleased/mr-conflict-notification.yml
deleted file mode 100644
index d3d5f1fc373..00000000000
--- a/changelogs/unreleased/mr-conflict-notification.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: When MR becomes unmergeable, notify and create todo for author and merge user
-merge_request: 18042
-author:
-type: added
diff --git a/changelogs/unreleased/n-plus-one-notification-recipients.yml b/changelogs/unreleased/n-plus-one-notification-recipients.yml
deleted file mode 100644
index 91c31e4c930..00000000000
--- a/changelogs/unreleased/n-plus-one-notification-recipients.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix some sources of excessive query counts when calculating notification recipients
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/new-label-spelling-error.yml b/changelogs/unreleased/new-label-spelling-error.yml
deleted file mode 100644
index ad5f69688f3..00000000000
--- a/changelogs/unreleased/new-label-spelling-error.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes a spelling error on the new label page
-merge_request: 19316
-author: samdbeckham
-type: fixed
diff --git a/changelogs/unreleased/no-multi-assign-enable.yml b/changelogs/unreleased/no-multi-assign-enable.yml
new file mode 100644
index 00000000000..bb9c69b18e7
--- /dev/null
+++ b/changelogs/unreleased/no-multi-assign-enable.yml
@@ -0,0 +1,5 @@
+---
+title: Enable no-multi-assignment in JS files
+merge_request: 19808
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/no-multi-assign-follow-up.yml b/changelogs/unreleased/no-multi-assign-follow-up.yml
new file mode 100644
index 00000000000..817760ff649
--- /dev/null
+++ b/changelogs/unreleased/no-multi-assign-follow-up.yml
@@ -0,0 +1,5 @@
+---
+title: Improve no-multi-assignment fixes after enabling rule
+merge_request: 19915
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/no-restricted-globals-enable.yml b/changelogs/unreleased/no-restricted-globals-enable.yml
new file mode 100644
index 00000000000..1fa2eac0d03
--- /dev/null
+++ b/changelogs/unreleased/no-restricted-globals-enable.yml
@@ -0,0 +1,5 @@
+---
+title: Enable no-restricted globals in JS files
+merge_request: 19877
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/optimise-pages-service-calling.yml b/changelogs/unreleased/optimise-pages-service-calling.yml
deleted file mode 100644
index e017e6b01f1..00000000000
--- a/changelogs/unreleased/optimise-pages-service-calling.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Optimise PagesWorker usage
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/optimise-paused-runners.yml b/changelogs/unreleased/optimise-paused-runners.yml
deleted file mode 100644
index 13097e507d3..00000000000
--- a/changelogs/unreleased/optimise-paused-runners.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Optimise paused runners to reduce amount of used requests
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/optimise-runner-update-cached-info.yml b/changelogs/unreleased/optimise-runner-update-cached-info.yml
deleted file mode 100644
index 15fb9bcdf41..00000000000
--- a/changelogs/unreleased/optimise-runner-update-cached-info.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update runner cached informations without performing validations
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/osw-delete-non-latest-mr-diff-files-upon-merge.yml b/changelogs/unreleased/osw-delete-non-latest-mr-diff-files-upon-merge.yml
new file mode 100644
index 00000000000..3e752125f3a
--- /dev/null
+++ b/changelogs/unreleased/osw-delete-non-latest-mr-diff-files-upon-merge.yml
@@ -0,0 +1,5 @@
+---
+title: Delete non-latest merge request diff files upon merge
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/osw-ignore-diff-header-when-persisting-diff-hunk.yml b/changelogs/unreleased/osw-ignore-diff-header-when-persisting-diff-hunk.yml
deleted file mode 100644
index ef66deaa0ef..00000000000
--- a/changelogs/unreleased/osw-ignore-diff-header-when-persisting-diff-hunk.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust insufficient diff hunks being persisted on NoteDiffFile
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/patch-28.yml b/changelogs/unreleased/patch-28.yml
deleted file mode 100644
index 1bbca138cae..00000000000
--- a/changelogs/unreleased/patch-28.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix FreeBSD can not upload artifacts due to wrong tmp path
-merge_request: 19148
-author:
-type: fixed
diff --git a/changelogs/unreleased/per-project-pipeline-iid.yml b/changelogs/unreleased/per-project-pipeline-iid.yml
deleted file mode 100644
index 78a513a9986..00000000000
--- a/changelogs/unreleased/per-project-pipeline-iid.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add per-project pipeline id
-merge_request: 18558
-author:
-type: added
diff --git a/changelogs/unreleased/pipelines-index-performance.yml b/changelogs/unreleased/pipelines-index-performance.yml
deleted file mode 100644
index 928c2ddab72..00000000000
--- a/changelogs/unreleased/pipelines-index-performance.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve performance of project pipelines pages
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/prefer-destructuring-fix.yml b/changelogs/unreleased/prefer-destructuring-fix.yml
new file mode 100644
index 00000000000..452e04f553e
--- /dev/null
+++ b/changelogs/unreleased/prefer-destructuring-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Enable prefer-structuring in JS files
+merge_request: 19943
+author: gfyoung
+type: other
diff --git a/changelogs/unreleased/presigned-multipart-uploads.yml b/changelogs/unreleased/presigned-multipart-uploads.yml
deleted file mode 100644
index 52fae6534fd..00000000000
--- a/changelogs/unreleased/presigned-multipart-uploads.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support direct_upload with S3 Multipart uploads
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/rails5-active-sup-subscriber.yml b/changelogs/unreleased/rails5-active-sup-subscriber.yml
deleted file mode 100644
index 439fa6f428e..00000000000
--- a/changelogs/unreleased/rails5-active-sup-subscriber.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make ActiveRecordSubscriber rails 5 compatible
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/rails5-fix-46230.yml b/changelogs/unreleased/rails5-fix-46230.yml
deleted file mode 100644
index 8ec28604483..00000000000
--- a/changelogs/unreleased/rails5-fix-46230.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use strings as properties key in kubernetes service spec.
-merge_request: 19265
-author: Jasper Maes
-type: fixed
diff --git a/changelogs/unreleased/rails5-fix-46236.yml b/changelogs/unreleased/rails5-fix-46236.yml
deleted file mode 100644
index 9203b448bed..00000000000
--- a/changelogs/unreleased/rails5-fix-46236.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support rails5 in postgres indexes function and fix some migrations
-merge_request: 19400
-author: Jasper Maes
-type: fixed
diff --git a/changelogs/unreleased/rails5-fix-46281.yml b/changelogs/unreleased/rails5-fix-46281.yml
deleted file mode 100644
index ee0b8531988..00000000000
--- a/changelogs/unreleased/rails5-fix-46281.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rails5 fix arel from
-merge_request: 19340
-author: Jasper Maes
-type: fixed
diff --git a/changelogs/unreleased/rails5-fix-47368.yml b/changelogs/unreleased/rails5-fix-47368.yml
deleted file mode 100644
index 81bb1adabff..00000000000
--- a/changelogs/unreleased/rails5-fix-47368.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: 'Rails 5 fix unknown keywords: changes, key_id, project, gl_repository, action,
- secret_token, protocol'
-merge_request: 19466
-author: Jasper Maes
-type: fixed
diff --git a/changelogs/unreleased/rails5-fix-47376.yml b/changelogs/unreleased/rails5-fix-47376.yml
deleted file mode 100644
index ac9950e908e..00000000000
--- a/changelogs/unreleased/rails5-fix-47376.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rails 5 fix glob spec
-merge_request: 19469
-author: Jasper Maes
-type: fixed
diff --git a/changelogs/unreleased/rails5-fix-48140.yml b/changelogs/unreleased/rails5-fix-48140.yml
new file mode 100644
index 00000000000..a6992803e5a
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-48140.yml
@@ -0,0 +1,6 @@
+---
+title: 'Rails 5 fix Capybara::ElementNotFound: Unable to find visible css #modal-revert-commit
+ and expected: "/bar" got: "/foo"'
+merge_request: 20044
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-fix-mysql-arel-from.yml b/changelogs/unreleased/rails5-fix-mysql-arel-from.yml
new file mode 100644
index 00000000000..9883ff306f1
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-mysql-arel-from.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix arel from in mysql_median_datetime_sql
+merge_request: 20167
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rd-44364-deprecate-support-for-dsa-keys.yml b/changelogs/unreleased/rd-44364-deprecate-support-for-dsa-keys.yml
deleted file mode 100644
index 1a52ffaaf79..00000000000
--- a/changelogs/unreleased/rd-44364-deprecate-support-for-dsa-keys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add migration to disable the usage of DSA keys
-merge_request: 19299
-author:
-type: other
diff --git a/changelogs/unreleased/reactive-caching-alive-bug.yml b/changelogs/unreleased/reactive-caching-alive-bug.yml
deleted file mode 100644
index 2fdc3a7e7e1..00000000000
--- a/changelogs/unreleased/reactive-caching-alive-bug.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Updates ReactiveCaching clear_reactive_caching method to clear both data and
- alive caching
-merge_request: 19311
-author:
-type: fixed
diff --git a/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml b/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml
deleted file mode 100644
index b8b2762a21d..00000000000
--- a/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move SquashBeforeMerge vue component
-merge_request: 18813
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml b/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml
deleted file mode 100644
index ddf7f51aa5e..00000000000
--- a/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove docker pull prefix from registry clipboard feature
-merge_request: 18933
-author: Lars Greiss
-type: changed
diff --git a/changelogs/unreleased/remove-unused-query-in-hooks.yml b/changelogs/unreleased/remove-unused-query-in-hooks.yml
deleted file mode 100644
index ef40b2db5a9..00000000000
--- a/changelogs/unreleased/remove-unused-query-in-hooks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove unused running_or_pending_build_count
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/rename-merge-request-widget-author-component.yml b/changelogs/unreleased/rename-merge-request-widget-author-component.yml
deleted file mode 100644
index 15e6eafd826..00000000000
--- a/changelogs/unreleased/rename-merge-request-widget-author-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rename merge request widget author component
-merge_request: 19079
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/revert-merge-request-widget-button-height.yml b/changelogs/unreleased/revert-merge-request-widget-button-height.yml
new file mode 100644
index 00000000000..7c400a4a2b2
--- /dev/null
+++ b/changelogs/unreleased/revert-merge-request-widget-button-height.yml
@@ -0,0 +1,5 @@
+---
+title: Revert merge request widget button max height
+merge_request: 20175
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/security-2682-fix-xss-for-markdown-toc.yml b/changelogs/unreleased/security-2682-fix-xss-for-markdown-toc.yml
new file mode 100644
index 00000000000..f595678c3c2
--- /dev/null
+++ b/changelogs/unreleased/security-2682-fix-xss-for-markdown-toc.yml
@@ -0,0 +1,5 @@
+---
+title: Fix XSS vulnerability for table of content generation
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-dm-delete-deploy-key.yml b/changelogs/unreleased/security-dm-delete-deploy-key.yml
deleted file mode 100644
index aa94e7b6072..00000000000
--- a/changelogs/unreleased/security-dm-delete-deploy-key.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix API to remove deploy key from project instead of deleting it entirely
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/security-fj-bumping-sanitize-gem.yml b/changelogs/unreleased/security-fj-bumping-sanitize-gem.yml
new file mode 100644
index 00000000000..bec1033425d
--- /dev/null
+++ b/changelogs/unreleased/security-fj-bumping-sanitize-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Update sanitize gem to 4.6.5 to fix HTML injection vulnerability
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fj-import-export-assignment.yml b/changelogs/unreleased/security-fj-import-export-assignment.yml
deleted file mode 100644
index 4bfd71d431a..00000000000
--- a/changelogs/unreleased/security-fj-import-export-assignment.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed bug that allowed importing arbitrary project attributes
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/security-html_escape_branch_name.yml b/changelogs/unreleased/security-html_escape_branch_name.yml
new file mode 100644
index 00000000000..02d1065348f
--- /dev/null
+++ b/changelogs/unreleased/security-html_escape_branch_name.yml
@@ -0,0 +1,5 @@
+---
+title: HTML escape branch name in project graphs page
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-html_escape_usernames.yml b/changelogs/unreleased/security-html_escape_usernames.yml
new file mode 100644
index 00000000000..7e69e4ae266
--- /dev/null
+++ b/changelogs/unreleased/security-html_escape_usernames.yml
@@ -0,0 +1,5 @@
+---
+title: HTML escape the name of the user in ProjectsHelper#link_to_member
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-rd-do-not-show-internal-info-in-public-feed.yml b/changelogs/unreleased/security-rd-do-not-show-internal-info-in-public-feed.yml
new file mode 100644
index 00000000000..ff78c162dff
--- /dev/null
+++ b/changelogs/unreleased/security-rd-do-not-show-internal-info-in-public-feed.yml
@@ -0,0 +1,5 @@
+---
+title: Don't show events from internal projects for anonymous users in public feed
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-users-can-update-their-password-without-entering-current-password.yml b/changelogs/unreleased/security-users-can-update-their-password-without-entering-current-password.yml
deleted file mode 100644
index 824fbd41ab8..00000000000
--- a/changelogs/unreleased/security-users-can-update-their-password-without-entering-current-password.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent user passwords from being changed without providing the previous password
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/sh-add-uncached-query-limiter.yml b/changelogs/unreleased/sh-add-uncached-query-limiter.yml
deleted file mode 100644
index 4318338c229..00000000000
--- a/changelogs/unreleased/sh-add-uncached-query-limiter.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove N+1 query for author in issues API
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-batch-dependent-destroys.yml b/changelogs/unreleased/sh-batch-dependent-destroys.yml
deleted file mode 100644
index e297badc1fa..00000000000
--- a/changelogs/unreleased/sh-batch-dependent-destroys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix project destruction failing due to idle in transaction timeouts
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml
deleted file mode 100644
index 145fdf72020..00000000000
--- a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump omniauth-gitlab to 1.0.3
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml b/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml
deleted file mode 100644
index aae42b66c84..00000000000
--- a/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add a unique and not null constraint on the project_features.project_id column
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-expire-content-cache-after-import.yml b/changelogs/unreleased/sh-expire-content-cache-after-import.yml
deleted file mode 100644
index 8876a487b86..00000000000
--- a/changelogs/unreleased/sh-expire-content-cache-after-import.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expire Wiki content cache after importing a repository
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml b/changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml
deleted file mode 100644
index d9bd1af9380..00000000000
--- a/changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix admin counters not working when PostgreSQL has secondaries
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-backup-specific-rake-task.yml b/changelogs/unreleased/sh-fix-backup-specific-rake-task.yml
deleted file mode 100644
index 71b121710ee..00000000000
--- a/changelogs/unreleased/sh-fix-backup-specific-rake-task.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix backup creation and restore for specific Rake tasks
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml b/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml
deleted file mode 100644
index 3c51aaae896..00000000000
--- a/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix cross-origin errors when attempting to download JavaScript attachments
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-events-nplus-one.yml b/changelogs/unreleased/sh-fix-events-nplus-one.yml
deleted file mode 100644
index e5a974bef30..00000000000
--- a/changelogs/unreleased/sh-fix-events-nplus-one.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Eliminate N+1 queries with authors and push_data_payload in Events API
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-fix-grape-logging-status-code.yml b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml
deleted file mode 100644
index aabf9a84bfb..00000000000
--- a/changelogs/unreleased/sh-fix-grape-logging-status-code.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix api_json.log not always reporting the right HTTP status code
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-api-perf-n-plus-one.yml b/changelogs/unreleased/sh-fix-issue-api-perf-n-plus-one.yml
deleted file mode 100644
index 57ba081326f..00000000000
--- a/changelogs/unreleased/sh-fix-issue-api-perf-n-plus-one.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Eliminate cached N+1 queries for projects in Issue API
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-fix-pipeline-jobs-nplus-one.yml b/changelogs/unreleased/sh-fix-pipeline-jobs-nplus-one.yml
deleted file mode 100644
index eac00f4fca6..00000000000
--- a/changelogs/unreleased/sh-fix-pipeline-jobs-nplus-one.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Eliminate N+1 queries for CI job artifacts in /api/prjoects/:id/pipelines/:pipeline_id/jobs
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-fix-secrets-not-working.yml b/changelogs/unreleased/sh-fix-secrets-not-working.yml
deleted file mode 100644
index 044a873ecd9..00000000000
--- a/changelogs/unreleased/sh-fix-secrets-not-working.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix attr_encryption key settings
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-source-project-nplus-one.yml b/changelogs/unreleased/sh-fix-source-project-nplus-one.yml
deleted file mode 100644
index 9d78ad6408c..00000000000
--- a/changelogs/unreleased/sh-fix-source-project-nplus-one.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix N+1 with source_projects in merge requests API
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-improve-import-status-error.yml b/changelogs/unreleased/sh-improve-import-status-error.yml
deleted file mode 100644
index 6523280f9e6..00000000000
--- a/changelogs/unreleased/sh-improve-import-status-error.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show a more helpful error for import status
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/sh-log-422-responses.yml b/changelogs/unreleased/sh-log-422-responses.yml
deleted file mode 100644
index c7dfdbb703b..00000000000
--- a/changelogs/unreleased/sh-log-422-responses.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Log response body to production_json.log when a controller responds with a
- 422 status
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/sh-move-delete-groups-api-async.yml b/changelogs/unreleased/sh-move-delete-groups-api-async.yml
deleted file mode 100644
index 1b200cac5c5..00000000000
--- a/changelogs/unreleased/sh-move-delete-groups-api-async.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move API group deletion to Sidekiq
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/sh-tag-queue-duration-api-calls.yml b/changelogs/unreleased/sh-tag-queue-duration-api-calls.yml
deleted file mode 100644
index 686cceaab62..00000000000
--- a/changelogs/unreleased/sh-tag-queue-duration-api-calls.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Log Workhorse queue duration for Grape API calls
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/sh-use-grape-path-helpers.yml b/changelogs/unreleased/sh-use-grape-path-helpers.yml
deleted file mode 100644
index c462c7e8194..00000000000
--- a/changelogs/unreleased/sh-use-grape-path-helpers.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace grape-route-helpers with our own grape-path-helpers
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sql-buckets.yml b/changelogs/unreleased/sql-buckets.yml
deleted file mode 100644
index afb13d5cb20..00000000000
--- a/changelogs/unreleased/sql-buckets.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust SQL and transaction Prometheus buckets
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml b/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml
deleted file mode 100644
index 6c378fd450a..00000000000
--- a/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for 'active' setting on Runner Registration API endpoint
-merge_request: 18848
-author:
-type: changed
diff --git a/changelogs/unreleased/update-external-link-icon-in-header-user-dropdown.yml b/changelogs/unreleased/update-external-link-icon-in-header-user-dropdown.yml
new file mode 100644
index 00000000000..ee769f06379
--- /dev/null
+++ b/changelogs/unreleased/update-external-link-icon-in-header-user-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Update external link icon in header user dropdown
+merge_request: 20150
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/update-help-integration-screenshot.yml b/changelogs/unreleased/update-help-integration-screenshot.yml
deleted file mode 100644
index b1f76a346e4..00000000000
--- a/changelogs/unreleased/update-help-integration-screenshot.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update screenshot in Gitlab.com integration documentation
-merge_request: 19433
-author: Tuğçe Nur Taş
-type: other
diff --git a/changelogs/unreleased/update-pipeline-icon-in-web-ide-sidebar.yml b/changelogs/unreleased/update-pipeline-icon-in-web-ide-sidebar.yml
new file mode 100644
index 00000000000..3f1f3c643e2
--- /dev/null
+++ b/changelogs/unreleased/update-pipeline-icon-in-web-ide-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Update pipeline icon in web ide sidebar
+merge_request: 20058
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/update-wiki-modal.yml b/changelogs/unreleased/update-wiki-modal.yml
deleted file mode 100644
index 00f2fc4f181..00000000000
--- a/changelogs/unreleased/update-wiki-modal.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: New design for wiki page deletion confirmation
-merge_request: 18712
-author: Constance Okoghenun
-type: added
diff --git a/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml
deleted file mode 100644
index 098e4b1d5fa..00000000000
--- a/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Use case in-sensitive ordering by name for dashboard"
-merge_request: 18553
-author: "@vedharish"
-type: fixed
diff --git a/changelogs/unreleased/winh-make-it-right-now.yml b/changelogs/unreleased/winh-make-it-right-now.yml
deleted file mode 100644
index 7b386c0b332..00000000000
--- a/changelogs/unreleased/winh-make-it-right-now.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use "right now" for short time periods
-merge_request: 19095
-author:
-type: changed
diff --git a/changelogs/unreleased/zj-add-branch-mandatory.yml b/changelogs/unreleased/zj-add-branch-mandatory.yml
deleted file mode 100644
index 82712ce842d..00000000000
--- a/changelogs/unreleased/zj-add-branch-mandatory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adding branches through the WebUI is handled by Gitaly
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-calculate-checksum-mandator.yml b/changelogs/unreleased/zj-calculate-checksum-mandator.yml
deleted file mode 100644
index 83315a3c5dd..00000000000
--- a/changelogs/unreleased/zj-calculate-checksum-mandator.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove shellout implementation for Repository checksums
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml b/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml
deleted file mode 100644
index 61bdce43c0e..00000000000
--- a/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Refs containting sha checks are done by Gitaly
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-wiki-find-file-opt-out.yml b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml
deleted file mode 100644
index 5af53c56017..00000000000
--- a/changelogs/unreleased/zj-wiki-find-file-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Finding a wiki page is done by Gitaly by default
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-workhorse-archive-mandatory.yml b/changelogs/unreleased/zj-workhorse-archive-mandatory.yml
deleted file mode 100644
index 3a4a351a2b9..00000000000
--- a/changelogs/unreleased/zj-workhorse-archive-mandatory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Workhorse will use Gitaly to create archives
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml b/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml
deleted file mode 100644
index bce68692d98..00000000000
--- a/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Workhorse to send raw diff and patch for commits
-merge_request:
-author:
-type: other
diff --git a/config/application.rb b/config/application.rb
index 385eb65e7f4..d9483cd806d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -5,6 +5,12 @@ require 'rails/all'
Bundler.require(:default, Rails.env)
module Gitlab
+ # This method is used for smooth upgrading from the current Rails 4.x to Rails 5.0.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/14286
+ def self.rails5?
+ ENV["RAILS5"].in?(%w[1 true])
+ end
+
class Application < Rails::Application
require_dependency Rails.root.join('lib/gitlab/redis/wrapper')
require_dependency Rails.root.join('lib/gitlab/redis/cache')
@@ -14,6 +20,11 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/current_settings')
require_dependency Rails.root.join('lib/gitlab/middleware/read_only')
+ # This needs to be loaded before DB connection is made
+ # to make sure that all connections have NO_ZERO_DATE
+ # setting disabled
+ require_dependency Rails.root.join('lib/mysql_zero_date')
+
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
@@ -62,7 +73,7 @@ module Gitlab
# namespaces/users.
# https://github.com/rails/rails/blob/5-0-stable/actioncable/lib/action_cable.rb#L38
# Please change this value when configuring ActionCable for real usage.
- config.action_cable.mount_path = "-" if rails5?
+ config.action_cable.mount_path = "/-/cable" if rails5?
# Configure sensitive parameters which will be filtered from the log file.
#
@@ -211,10 +222,4 @@ module Gitlab
Gitlab::Routing.add_helpers(MilestonesRoutingHelper)
end
end
-
- # This method is used for smooth upgrading from the current Rails 4.x to Rails 5.0.
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/14286
- def self.rails5?
- ENV["RAILS5"].in?(%w[1 true])
- end
end
diff --git a/config/initializers/active_record_data_types.rb b/config/initializers/active_record_data_types.rb
index fda13d0c4cb..717e30b5b7e 100644
--- a/config/initializers/active_record_data_types.rb
+++ b/config/initializers/active_record_data_types.rb
@@ -65,7 +65,7 @@ elsif Gitlab::Database.mysql?
prepend RegisterDateTimeWithTimeZone
# Add the class `DateTimeWithTimeZone` so we can map `timestamp` to it.
- class MysqlDateTimeWithTimeZone < MysqlDateTime
+ class MysqlDateTimeWithTimeZone < (Gitlab.rails5? ? ActiveRecord::Type::DateTime : MysqlDateTime)
def type
:datetime_with_timezone
end
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 362b9cc9a88..d051b699102 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -219,5 +219,7 @@ Devise.setup do |config|
end
end
- Gitlab::OmniauthInitializer.new(config).execute(Gitlab.config.omniauth.providers)
+ if Gitlab.config.omniauth.enabled
+ Gitlab::OmniauthInitializer.new(config).execute(Gitlab.config.omniauth.providers)
+ end
end
diff --git a/config/karma.config.js b/config/karma.config.js
index 28a688797d9..84810332dc2 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -15,6 +15,7 @@ function fatalError(message) {
// disable problematic options
webpackConfig.entry = undefined;
webpackConfig.mode = 'development';
+webpackConfig.optimization.nodeEnv = false;
webpackConfig.optimization.runtimeChunk = false;
webpackConfig.optimization.splitChunks = false;
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 889111282ef..9f451046462 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -60,17 +60,23 @@ en:
scopes:
api: Access the authenticated user's API
read_user: Read the authenticated user's personal information
+ read_repository: Allows read-access to the repository
+ read_registry: Grants permission to read container registry images
openid: Authenticate using OpenID Connect
- sudo: Perform API actions as any user in the system (if the authenticated user is an admin)
+ sudo: Perform API actions as any user in the system
scope_desc:
api:
- Full access to GitLab as the user, including read/write on all their groups and projects
+ Grants complete read/write access to the API, including all groups and projects.
read_user:
- Read-only access to the user's profile information, like username, public email and full name
+ Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users.
+ read_repository:
+ Grants read-only access to repositories on private projects using Git-over-HTTP (not using the API).
+ read_registry:
+ Grants read-only access to container registry images on private projects.
openid:
- The ability to authenticate using GitLab, and read-only access to the user's profile information and group memberships
+ Grants permission to authenticate with GitLab using OpenID Connect. Also gives read-only access to the user's profile and group memberships.
sudo:
- Access to the Sudo feature, to perform API actions as any user in the system (only available for admins)
+ Grants permission to perform API actions as any user in the system, when authenticated as an admin user.
flash:
applications:
create:
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index d16060e8f45..3400142db36 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -76,4 +76,5 @@
- [repository_update_remote_mirror, 1]
- [repository_remove_remote, 1]
- [create_note_diff_file, 1]
+ - [delete_diff_files, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index b1e378f6c27..583f05f2fb7 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -16,10 +16,13 @@ const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false';
const WEBPACK_REPORT = process.env.WEBPACK_REPORT;
const NO_COMPRESSION = process.env.NO_COMPRESSION;
+const NO_SOURCEMAPS = process.env.NO_SOURCEMAPS;
const VUE_VERSION = require('vue/package.json').version;
const VUE_LOADER_VERSION = require('vue-loader/package.json').version;
+const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map';
+
let autoEntriesCount = 0;
let watchAutoEntries = [];
const defaultEntries = ['./main'];
@@ -171,7 +174,6 @@ module.exports = {
},
optimization: {
- nodeEnv: false,
runtimeChunk: 'single',
splitChunks: {
maxInitialRequests: 4,
@@ -286,7 +288,7 @@ module.exports = {
inline: DEV_SERVER_LIVERELOAD,
},
- devtool: IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map',
+ devtool: NO_SOURCEMAPS ? false : devtool,
// sqljs requires fs
node: { fs: 'empty' },
diff --git a/db/migrate/20180626125654_add_index_on_deployable_for_deployments.rb b/db/migrate/20180626125654_add_index_on_deployable_for_deployments.rb
new file mode 100644
index 00000000000..a0e3a228f6c
--- /dev/null
+++ b/db/migrate/20180626125654_add_index_on_deployable_for_deployments.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexOnDeployableForDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :deployments, [:deployable_type, :deployable_id]
+ end
+
+ def down
+ remove_concurrent_index :deployments, [:deployable_type, :deployable_id]
+ end
+end
diff --git a/db/migrate/merge_request_diff_file_limits_to_mysql.rb b/db/migrate/merge_request_diff_file_limits_to_mysql.rb
index 3958380e4b9..ca3bc7d6be9 100644
--- a/db/migrate/merge_request_diff_file_limits_to_mysql.rb
+++ b/db/migrate/merge_request_diff_file_limits_to_mysql.rb
@@ -4,7 +4,7 @@ class MergeRequestDiffFileLimitsToMysql < ActiveRecord::Migration
def up
return unless Gitlab::Database.mysql?
- change_column :merge_request_diff_files, :diff, :text, limit: 2147483647
+ change_column :merge_request_diff_files, :diff, :text, limit: 2147483647, default: nil
end
def down
diff --git a/db/schema.rb b/db/schema.rb
index d05c6afbb9f..0112fc726d4 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: 20180608201435) do
+ActiveRecord::Schema.define(version: 20180626125654) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -757,6 +757,7 @@ ActiveRecord::Schema.define(version: 20180608201435) do
end
add_index "deployments", ["created_at"], name: "index_deployments_on_created_at", using: :btree
+ add_index "deployments", ["deployable_type", "deployable_id"], name: "index_deployments_on_deployable_type_and_deployable_id", using: :btree
add_index "deployments", ["environment_id", "id"], name: "index_deployments_on_environment_id_and_id", using: :btree
add_index "deployments", ["environment_id", "iid", "project_id"], name: "index_deployments_on_environment_id_and_iid_and_project_id", using: :btree
add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree
diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md
index f0b2054a7f3..a5cd2b642dc 100644
--- a/doc/administration/job_traces.md
+++ b/doc/administration/job_traces.md
@@ -134,4 +134,4 @@ We're currently evaluating this feature on dev.gitalb.org or staging.gitlab.com
- TBD
-[ce-44935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18169
+[ce-18169]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18169 \ No newline at end of file
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 411a0fae93f..cea6764df41 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -49,6 +49,20 @@ The following metrics are available:
| filesystem_circuitbreaker | Gauge | 9.5 | Whether or not the circuit for a certain shard is broken or not |
| circuitbreaker_storage_check_duration_seconds | Histogram | 10.3 | Time a single storage probe took |
+### Ruby metrics
+
+Some basic Ruby runtime metrics are available:
+
+| Metric | Type | Since | Description |
+|:-------------------------------------- |:--------- |:----- |:----------- |
+| ruby_gc_duration_seconds_total | Counter | 11.1 | Time spent by Ruby in GC |
+| ruby_gc_stat_... | Gauge | 11.1 | Various metrics from [GC.stat] |
+| ruby_file_descriptors | Gauge | 11.1 | File descriptors per process |
+| ruby_memory_bytes | Gauge | 11.1 | Memory usage by process |
+| ruby_sampler_duration_seconds_total | Counter | 11.1 | Time spent collecting stats |
+
+[GC.stat]: https://ruby-doc.org/core-2.3.0/GC.html#method-c-stat
+
## Metrics shared directory
GitLab's Prometheus client requires a directory to store metrics data shared between multi-process services.
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 3ca07ce9795..ac814bbf19a 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -30,6 +30,7 @@ Example response:
"description": "test-1-20150125",
"id": 6,
"is_shared": false,
+ "ip_address": "127.0.0.1",
"name": null,
"online": true,
"status": "online"
@@ -38,6 +39,7 @@ Example response:
"active": true,
"description": "test-2-20150125",
"id": 8,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"name": null,
"online": false,
@@ -72,6 +74,7 @@ Example response:
"active": true,
"description": "shared-runner-1",
"id": 1,
+ "ip_address": "127.0.0.1",
"is_shared": true,
"name": null,
"online": true,
@@ -81,6 +84,7 @@ Example response:
"active": true,
"description": "shared-runner-2",
"id": 3,
+ "ip_address": "127.0.0.1",
"is_shared": true,
"name": null,
"online": false
@@ -90,6 +94,7 @@ Example response:
"active": true,
"description": "test-1-20150125",
"id": 6,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"name": null,
"online": true
@@ -99,6 +104,7 @@ Example response:
"active": true,
"description": "test-2-20150125",
"id": 8,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"name": null,
"online": false,
@@ -131,6 +137,7 @@ Example response:
"architecture": null,
"description": "test-1-20150125",
"id": 6,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"contacted_at": "2016-01-25T16:39:48.066Z",
"name": null,
@@ -189,6 +196,7 @@ Example response:
"architecture": null,
"description": "test-1-20150125-test",
"id": 6,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"contacted_at": "2016-01-25T16:39:48.066Z",
"name": null,
@@ -257,6 +265,7 @@ Example response:
[
{
"id": 2,
+ "ip_address": "127.0.0.1",
"status": "running",
"stage": "test",
"name": "test",
@@ -345,6 +354,7 @@ Example response:
"active": true,
"description": "test-2-20150125",
"id": 8,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"name": null,
"online": false,
@@ -354,6 +364,7 @@ Example response:
"active": true,
"description": "development_runner",
"id": 5,
+ "ip_address": "127.0.0.1",
"is_shared": true,
"name": null,
"online": true
@@ -386,6 +397,7 @@ Example response:
"active": true,
"description": "test-2016-02-01",
"id": 9,
+ "ip_address": "127.0.0.1",
"is_shared": false,
"name": null,
"online": true,
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 48e1685082a..f5cdd310f6f 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -322,50 +322,49 @@ to EE only.
## Previewing the changes live
-To preview your changes to documentation locally, please follow
-this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
+NOTE: **Note:**
+To preview your changes to documentation locally, follow this
+[development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
-If you want to preview the doc changes of your merge request live, you can use
-the manual `review-docs-deploy` job in your merge request. You will need at
-least Maintainer permissions to be able to run it and is currently enabled for the
-following projects:
+The live preview is currently enabled for the following projects:
- https://gitlab.com/gitlab-org/gitlab-ce
- https://gitlab.com/gitlab-org/gitlab-ee
+- https://gitlab.com/gitlab-org/gitlab-runner
-NOTE: **Note:**
-You will need to push a branch to those repositories, it doesn't work for forks.
-
-TIP: **Tip:**
If your branch contains only documentation changes, you can use
[special branch names](#branch-naming) to avoid long running pipelines.
-In the mini pipeline graph, you should see an `>>` icon. Clicking on it will
-reveal the `review-docs-deploy` job. Hit the play button for the job to start.
+For [docs-only changes](#branch-naming), the review app is run automatically.
+For all other branches, you can use the manual `review-docs-deploy-manual` job
+in your merge request. You will need at least Maintainer permissions to be able
+to run it. In the mini pipeline graph, you should see an `>>` icon. Clicking on it will
+reveal the `review-docs-deploy-manual` job. Hit the play button for the job to start.
![Manual trigger a docs build](img/manual_build_docs.png)
-This job will:
+NOTE: **Note:**
+You will need to push a branch to those repositories, it doesn't work for forks.
+
+The `review-docs-deploy*` job will:
1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs)
- project named after the scheme: `preview-<branch-slug>`
+ project named after the scheme: `$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG`,
+ where `DOCS_GITLAB_REPO_SUFFIX` is the suffix for each product, e.g, `ce` for
+ CE, etc.
1. Trigger a cross project pipeline and build the docs site with your changes
After a few minutes, the Review App will be deployed and you will be able to
preview the changes. The docs URL can be found in two places:
- In the merge request widget
-- In the output of the `review-docs-deploy` job, which also includes the
+- In the output of the `review-docs-deploy*` job, which also includes the
triggered pipeline so that you can investigate whether something went wrong
In case the Review App URL returns 404, follow these steps to debug:
1. **Did you follow the URL from the merge request widget?** If yes, then check if
- the link is the same as the one in the job output. It can happen that if the
- branch name slug is longer than 35 characters, it is automatically
- truncated. That means that the merge request widget will not show the proper
- URL due to a limitation of how `environment: url` works, but you can find the
- real URL from the output of the `review-docs-deploy` job.
+ the link is the same as the one in the job output.
1. **Did you follow the URL from the job output?** If yes, then it means that
either the site is not yet deployed or something went wrong with the remote
pipeline. Give it a few minutes and it should appear online, otherwise you
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index e7ffba635c9..a315cfdc116 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -26,7 +26,11 @@ Check the GitLab handbook for the [writing styles guidelines](https://about.gitl
- Jump a line between different markups (e.g., after every paragraph, header, list, etc)
- Capitalize "G" and "L" in GitLab
- Use sentence case for titles, headings, labels, menu items, and buttons.
-- Use title case when referring to [features](https://about.gitlab.com/features/) or [products](https://about.gitlab.com/pricing/), and methods. Note that some features are also objects (e.g. "Merge Requests" and "merge requests"). E.g.: GitLab Runner, Geo, Issue Boards, Git, Prometheus, Continuous Integration.
+- Use title case when referring to [features](https://about.gitlab.com/features/) or
+[products](https://about.gitlab.com/pricing/) (e.g., GitLab Runner, Geo,
+Issue Boards, GitLab Core, Git, Prometheus, Kubernetes, etc), and methods or methodologies
+(e.g., Continuous Integration, Continuous Deployment, Scrum, Agile, etc). Note that
+some features are also objects (e.g. "Merge Requests" and "merge requests").
## Formatting
@@ -72,7 +76,7 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl
This is to ensure that no document with wrong heading is going
live without an audit, thus preventing dead links and redirection issues when
corrected
-- Leave exactly one newline after a heading
+- Leave exactly one new line after a heading
## Links
@@ -95,6 +99,16 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl
E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`,
write `Read more about [GitLab Issue Boards](LINK)`.
+## Navigation
+
+To indicate the steps of navigation through the UI:
+
+- Use the exact word as shown in the UI, including any capital letters as-is
+- Use bold text for navigation items and the char `>` as separator
+(e.g., `Navigate to your project's **Settings > CI/CD**` )
+- If there are any expandable menus, make sure to mention that the user
+needs to expand the tab to find the settings you're referring to
+
## Images
- Place images in a separate directory named `img/` in the same directory where
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 858b03c60bf..4089cd37d73 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -78,7 +78,7 @@ In this file, we will write the actions that will call the respective mutations:
```javascript
import * as types from './mutation_types';
- import axios from '~/lib/utils/axios-utils';
+ import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
export const requestUsers = ({ commit }) => commit(types.REQUEST_USERS);
@@ -214,7 +214,7 @@ import { mapGetters } from 'vuex';
};
```
-### `mutations_types.js`
+### `mutation_types.js`
From [vuex mutations docs][vuex-mutations]:
> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application.
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 5786287d00c..d25d856c3a3 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -92,6 +92,54 @@ describe API::Labels do
end
```
+## Avoid using `allow_any_instance_of` in RSpec
+
+### Why
+
+* Because it is not isolated therefore it might be broken at times.
+* Because it doesn't work whenever the method we want to stub was defined
+ in a prepended module, which is very likely the case in EE. We could see
+ error like this:
+
+ 1.1) Failure/Error: allow_any_instance_of(ApplicationSetting).to receive_messages(messages)
+ Using `any_instance` to stub a method (elasticsearch_indexing) that has been defined on a prepended module (EE::ApplicationSetting) is not supported.
+
+### Alternative: `expect_next_instance_of`
+
+Instead of writing:
+
+```ruby
+# Don't do this:
+allow_any_instance_of(Project).to receive(:add_import_job)
+```
+
+We could write:
+
+```ruby
+# Do this:
+expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
+end
+```
+
+If we also want to expect the instance was initialized with some particular
+arguments, we could also pass it to `expect_next_instance_of` like:
+
+```ruby
+# Do this:
+expect_next_instance_of(MergeRequests::RefreshService, project, user) do |refresh_service|
+ expect(refresh_service).to receive(:execute).with(oldrev, newrev, ref)
+end
+```
+
+This would expect the following:
+
+```ruby
+# Above expects:
+refresh_service = MergeRequests::RefreshService.new(project, user)
+refresh_service.execute(oldrev, newrev, ref)
+```
+
## Do not `rescue Exception`
See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception].
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index 9a677bf09b2..9d0d7348df9 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -24,6 +24,7 @@ are very appreciative of the work done by translators and proofreaders!
- Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo)
- Japanese
- Yamana Tokiuji - [GitLab](https://gitlab.com/tokiuji), [Crowdin](https://crowdin.com/profile/yamana)
+ - Hiroyuki Sato - [GitLab](https://gitlab.com/hiroponz), [Crowdin](https://crowdin.com/profile/hiroponz)
- Korean
- Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang)
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
@@ -31,6 +32,7 @@ are very appreciative of the work done by translators and proofreaders!
- Filip Mech - [GitLab](https://gitlab.com/mehenz), [Crowdin](https://crowdin.com/profile/mehenz)
- Portuguese, Brazilian
- Paulo George Gomes Bezerra - [GitLab](https://gitlab.com/paulobezerra), [Crowdin](https://crowdin.com/profile/paulogomes.rep)
+ - André Gama - [GitLab](https://gitlab.com/andregamma), [Crowdin](https://crowdin.com/profile/ToeOficial)
- Russian
- Nikita Grylov - [GitLab](https://gitlab.com/nixel2007), [Crowdin](https://crowdin.com/profile/nixel2007)
- Alexy Lustin - [GitLab](https://gitlab.com/allustin), [Crowdin](https://crowdin.com/profile/lustin)
diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md
index 61e5e1afede..2167ed57428 100644
--- a/doc/development/query_recorder.md
+++ b/doc/development/query_recorder.md
@@ -28,6 +28,7 @@ By default, QueryRecorder will ignore cached queries in the count. However, it m
all queries to avoid introducing an N+1 query that may be masked by the statement cache. To do this,
pass the `skip_cached` variable to `QueryRecorder` and use the `exceed_all_query_limit` matcher:
+```
it "avoids N+1 database queries" do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { visit_some_page }.count
create_list(:issue, 5)
diff --git a/doc/development/utilities.md b/doc/development/utilities.md
index 8f9aff1a35f..0d074a3ef05 100644
--- a/doc/development/utilities.md
+++ b/doc/development/utilities.md
@@ -135,3 +135,44 @@ We developed a number of utilities to ease development.
Find.new.clear_memoization(:result)
```
+
+## [`RequestCache`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/cache/request_cache.rb)
+
+This module provides a simple way to cache values in RequestStore,
+and the cache key would be based on the class name, method name,
+optionally customized instance level values, optionally customized
+method level values, and optional method arguments.
+
+A simple example that only uses the instance level customised values:
+
+``` ruby
+class UserAccess
+ extend Gitlab::Cache::RequestCache
+
+ request_cache_key do
+ [user&.id, project&.id]
+ end
+
+ request_cache def can_push_to_branch?(ref)
+ # ...
+ end
+end
+```
+
+This way, the result of `can_push_to_branch?` would be cached in
+`RequestStore.store` based on the cache key. If `RequestStore` is not
+currently active, then it would be stored in a hash saved in an
+instance variable, so the cache logic would be the same.
+
+We can also set different strategies for different methods:
+
+``` ruby
+class Commit
+ extend Gitlab::Cache::RequestCache
+
+ def author
+ User.find_by_any_email(author_email.downcase)
+ end
+ request_cache(:author) { author_email.downcase }
+end
+```
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index f502866333e..47396666879 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -195,22 +195,22 @@ end
And that's it, we're done!
-## Changing Column Types For Large Tables
+## Changing The Schema For Large Tables
-While `change_column_type_concurrently` can be used for changing the type of a
-column without downtime it doesn't work very well for large tables. Because all
-of the work happens in sequence the migration can take a very long time to
-complete, preventing a deployment from proceeding.
-`change_column_type_concurrently` can also produce a lot of pressure on the
-database due to it rapidly updating many rows in sequence.
+While `change_column_type_concurrently` and `rename_column_concurrently` can be
+used for changing the schema of a table without downtime it doesn't work very
+well for large tables. Because all of the work happens in sequence the migration
+can take a very long time to complete, preventing a deployment from proceeding.
+They can also produce a lot of pressure on the database due to it rapidly
+updating many rows in sequence.
To reduce database pressure you should instead use
-`change_column_type_using_background_migration` when migrating a column in a
-large table (e.g. `issues`). This method works similar to
-`change_column_type_concurrently` but uses background migration to spread the
-work / load over a longer time period, without slowing down deployments.
+`change_column_type_using_background_migration` or `rename_column_concurrently`
+when migrating a column in a large table (e.g. `issues`). These methods work
+similarly to the concurrent counterparts but uses background migration to spread
+the work / load over a longer time period, without slowing down deployments.
-Usage of this method is fairly simple:
+For example, to change the column type using a background migration:
```ruby
class ExampleMigration < ActiveRecord::Migration
@@ -296,6 +296,15 @@ class MigrateRemainingIssuesClosedAt < ActiveRecord::Migration
end
```
+The same applies to `rename_column_using_background_migration`:
+
+1. Create a migration using the helper, which will schedule background
+ migrations to spread the writes over a longer period of time.
+2. In the next monthly release, create a clean-up migration to steal from the
+ Sidekiq queues, migrate any missing rows, and cleanup the rename. This
+ migration should skip the steps after stealing from the Sidekiq queues if the
+ column has already been renamed.
+
For more information, see [the documentation on cleaning up background
migrations](background_migrations.md#cleaning-up).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index ef415246583..e4011b1a4ab 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -12,9 +12,8 @@ Since installations from source don't have Runit, Sidekiq can't be terminated an
## Select Version to Install
-Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) 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 in the top left corner of GitLab (below the menu bar).
+Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-0-stable`).
+You can select the branch in the version dropdown in 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/) for installation guide links by version.
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index 429519a92e1..48f3df1925a 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -1,6 +1,137 @@
# GitLab Helm Chart
-> **Note:** This chart is currently in alpha.
+> **Note:** The chart is currently **beta**, if you encounter any problems please [open an issue](https://gitlab.com/charts/gitlab/issues/new).
-The cloud native `gitlab` chart is the next generation Helm chart, currently in alpha, and will replace the [`gitlab-omnibus`](gitlab_omnibus.md) chart. It will support large deployments with horizontal scaling of individual GitLab components.
+For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
-Installation instructions and known issues during alpha are available at the [project page](https://gitlab.com/charts/gitlab/). \ No newline at end of file
+## Introduction
+
+The `gitlab` chart is the best way to operate GitLab on Kubernetes. This chart contains all the required components to get started, and can scale to large deployments.
+
+The default deployment includes:
+
+- Core GitLab components: Unicorn, Shell, Workhorse, Registry, Sidekiq, and Gitaly
+- Optional dependencies: Postgres, Redis, Minio
+- An auto-scaling, unprivileged [GitLab Runner](https://docs.gitlab.com/runner/) using the Kubernetes executor
+- Automatically provisioned SSL via [Let's Encrypt](https://letsencrypt.org/).
+
+### Limitations
+
+Some features and functions are not currently available in the beta release:
+* [GitLab Pages](../../user/project/pages/)
+* [Reply by email](../../administration/reply_by_email.html)
+* [Project templates](../../gitlab-basics/create-project.html)
+* [Project import/export](../../user/project/settings/import_export.html)
+* [Geo](https://docs.gitlab.com/ee/administration/geo/replication/)
+
+Currently out of scope:
+* [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/)
+* [MySQL support](https://docs.gitlab.com/omnibus/settings/database.html#using-a-mysql-database-management-server-enterprise-edition-only)
+
+## Prerequisites
+
+In order to deploy GitLab on Kubernetes, a few prerequisites are required.
+
+1. `helm` and `kubectl` [installed on your computer](preparation/tools_installation.md).
+1. A Kubernetes cluster, version 1.8 or higher. 6vCPU and 16GB of RAM is recommended.
+ * [Google GKE](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-container-cluster)
+ * [Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html)
+ * [Microsoft AKS](https://docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough-portal)
+1. A [wildcard DNS entry and external IP address](preparation/networking.md)
+1. [Authenticate and connect](preparation/connect.md) to the cluster
+1. Configure and initialize [Helm Tiller](preparation/tiller.md).
+
+## Configuring and Installing GitLab
+
+> **Note**: For deployments to Amazon EKS, there are [additional configuration requirements](preparation/eks.md).
+
+For simple deployments, running all services within Kubernetes, only three parameters are required:
+- `global.hosts.domain`: the [base domain](preparation/networking.md) of the wildcard host entry. For example, `mycompany.io` if the wild card entry is `*.mycompany.io`.
+- `global.hosts.externalIP`: the [external IP](preparation/networking.md) which the wildcard DNS resolves to.
+- `certmanager-issuer.email`: Email address to use when requesting new SSL certificates from Let's Encrypt.
+
+For enterprise deployments, or to utilize advanced settings, please use the instructions in the [`gitlab` chart project](https://gitlab.com/charts/gitlab) for the most up to date directions.
+- [External Postgres, Redis, and other dependencies](https://gitlab.com/charts/gitlab/tree/master/doc/advanced)
+- [Persistence settings](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md)
+- [Manual TLS certificates](https://gitlab.com/charts/gitlab/blob/master/doc/installation/tls.md)
+- [Manual secret creation](https://gitlab.com/charts/gitlab/blob/master/doc/installation/secrets.md)
+
+For additional configuration options, consult the [full list of settings](https://gitlab.com/charts/gitlab/blob/master/doc/installation/command-line-options.md).
+
+## Installing GitLab using the Helm Chart
+
+Once you have all of your configuration options collected, we can get any dependencies and
+run helm. In this example, we've named our helm release "gitlab".
+
+```
+helm repo add gitlab https://charts.gitlab.io/
+helm update
+helm upgrade --install gitlab gitlab/gitlab \
+ --timeout 600 \
+ --set global.hosts.domain=example.local \
+ --set global.hosts.externalIP=10.10.10.10 \
+ --set certmanager-issuer.email=me@example.local
+```
+
+### Monitoring the Deployment
+
+This will output the list of resources installed once the deployment finishes which may take 5-10 minutes.
+
+The status of the deployment can be checked by running `helm status gitlab` which can also be done while
+the deployment is taking place if you run the command in another terminal.
+
+### Initial login
+
+You can access the GitLab instance by visiting the domain name beginning with `gitlab.` followed by the domain specified during installation. From the example above, the URL would be `https://gitlab.example.local`.
+
+If you manually created the secret for initial root password, you
+can use that to sign in as `root` user. If not, Gitlab automatically
+created a random password for `root` user. This can be extracted by the
+following command (replace `<name>` by name of the release - which is `gitlab`
+if you used the command above).
+
+```
+kubectl get secret <name>-gitlab-initial-root-password -ojsonpath={.data.password} | base64 --decode
+```
+
+## Outgoing email
+
+By default outgoing email is disabled. To enable it, provide details for your SMTP server
+using the `global.smtp` and `global.email` settings. You can find details for these settings in the
+[command line options](https://gitlab.com/charts/gitlab/blob/master/doc/installation/command-line-options.md#email-configuration).
+
+If your SMTP server requires authentication make sure to read the section on providing
+your password in the [secrets documentation](https://gitlab.com/charts/gitlab/blob/master/doc/installation/secrets.md#smtp-password).
+You can disable authentication settings with `--set global.smtp.authentication=""`.
+
+If your Kubernetes cluster is on GKE, be aware that smtp [ports 25, 465, and 587
+are blocked](https://cloud.google.com/compute/docs/tutorials/sending-mail/#using_standard_email_ports).
+
+## Deploying the Community Edition
+
+To deploy the Community Edition, include these options in your `helm install` command:
+
+```shell
+--set gitlab.migrations.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-rails-ce
+--set gitlab.sidekiq.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-sidekiq-ce
+--set gitlab.unicorn.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-unicorn-ce
+```
+
+## Updating GitLab using the Helm Chart
+
+Once your GitLab Chart is installed, configuration changes and chart updates
+should be done using `helm upgrade`:
+
+```bash
+helm upgrade -f values.yaml gitlab gitlab/gitlab
+```
+
+## Uninstalling GitLab using the Helm Chart
+
+To uninstall the GitLab Chart, run the following:
+
+```bash
+helm delete gitlab
+```
+
+[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types
+[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index aeaa739edab..6419a9dcb69 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -4,7 +4,7 @@ description: 'Read through the different methods to deploy GitLab on Kubernetes.
# Installing GitLab on Kubernetes
-> **Note**: These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
+> **Note**: These charts have been tested on Google Kubernetes Engine. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/issues).
The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is
to take advantage of GitLab's Helm charts. [Helm] is a package
@@ -14,51 +14,44 @@ should be deployed, upgraded, and configured.
## Chart Overview
-* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small deployments. The chart is in beta and will be deprecated by the [cloud native GitLab chart](#cloud-native-gitlab-chart).
-* **[Cloud Native GitLab Chart](https://gitlab.com/charts/gitlab/blob/master/README.md)**: The next generation GitLab chart, currently in alpha. Will support large deployments with horizontal scaling of individual GitLab components.
+* **[GitLab Chart](gitlab_chart.html)**: The recommended GitLab chart, currently in beta. Supports large deployments with horizontal scaling of individual GitLab components, and does not require NFS.
+* **[GitLab Runner Chart](gitlab_runner_chart.md)**: For deploying just the GitLab Runner.
* Other Charts
- * [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner.
+ * [GitLab-Omnibus](gitlab_omnibus.md): Chart based on the Omnibus GitLab linux package, only suitable for small deployments. The chart will be deprecated by the [GitLab chart](#gitlab-chart) when it is GA.
* [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart.
-## GitLab-Omnibus Chart (Recommended)
+## GitLab Chart
-> **Note**: This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being added.
+> **Note**: This chart is **beta**, while we work on the [remaining items for GA](https://gitlab.com/groups/charts/-/epics/15).
-This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](../../user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html).
+The best way to operate GitLab on Kubernetes. This chart contains all the required components to get started, and can scale to large deployments.
-Once the [cloud native GitLab chart](#cloud-native-gitlab-chart) is ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment.
-
-Learn more about the [gitlab-omnibus chart](gitlab_omnibus.md).
-
-## Cloud Native GitLab Chart
-
-GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/gitlab/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current chart](#gitlab-omnibus-chart-recommended).
-
-By offering individual containers and charts, we will be able to provide a number of benefits:
-* Easier horizontal scaling of each service,
-* Smaller, more efficient images,
-* Potential for rolling updates and canaries within a service,
+This chart offers a number of benefits:
+* Horizontal scaling of individual components
+* No requirement for shared storage to scale
+* Containers do not need `root` permissions
+* Automatic SSL with Let's Encrypt
* and plenty more.
-Presently this chart is available in alpha for testing, and not recommended for production use.
+Learn more about the [GitLab chart here](gitlab_chart.md) and [here [Video]](https://youtu.be/Z6jWR8Z8dv8).
-Learn more about the [cloud native GitLab chart here ](https://gitlab.com/charts/gitlab/blob/master/README.md) and [here [Video]](https://youtu.be/Z6jWR8Z8dv8).
+## GitLab Runner Chart
-## Other Charts
+If you already have a GitLab instance running, inside or outside of Kubernetes, and you'd like to leverage the Runner's [Kubernetes capabilities](https://docs.gitlab.com/runner/executors/kubernetes.html), it can be deployed with the GitLab Runner chart.
-### GitLab Runner Chart
+Learn more about [gitlab-runner chart](gitlab_runner_chart.md).
-If you already have a GitLab instance running, inside or outside of Kubernetes, and you'd like to leverage the Runner's [Kubernetes capabilities](https://docs.gitlab.com/runner/executors/kubernetes.html), it can be deployed with the GitLab Runner chart.
+## Other Charts
-Learn more about [gitlab-runner chart.](gitlab_runner_chart.md)
+### GitLab-Omnibus Chart
-### Advanced GitLab Installation
+> **Note**: This chart is beta, and **will be deprecated** when the [`gitlab`](#gitlab-chart) chart is GA.
-If advanced configuration of GitLab is required, the beta [gitlab](gitlab_chart.md) chart can be used which deploys the core GitLab service along with optional Postgres and Redis. It offers extensive configuration, but offers limited functionality out-of-the-box; it's lacking Pages support, the container registry, and Mattermost. It requires deep knowledge of Kubernetes and Helm to use.
+It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](../../user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html).
-This chart will be deprecated and replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). It's beta quality, and since it is not actively under development, it will never be GA.
+Once the [GitLab chart](#gitlab-chart) is GA, this chart will be deprecated. Migrating to the `gitlab` chart will require exporting data out of this instance and importing it into a new deployment.
-Learn more about the [gitlab chart.](gitlab_chart.md)
+Learn more about the [gitlab-omnibus chart](gitlab_omnibus.md).
### Community Contributed Charts
diff --git a/doc/install/kubernetes/preparation/connect.md b/doc/install/kubernetes/preparation/connect.md
new file mode 100644
index 00000000000..fb633c456f5
--- /dev/null
+++ b/doc/install/kubernetes/preparation/connect.md
@@ -0,0 +1,31 @@
+# Connecting your computer to a cluster
+
+In order to deploy software and settings to a cluster, you must connect and authenticate to it.
+
+* [GKE cluster](#connect-to-gke-cluster)
+* [EKS cluster](#connect-to-eks-cluster)
+* [Local minikube cluster](#connect-to-local-minikube-cluster)
+
+## Connect to GKE cluster
+
+The command for connection to the cluster can be obtained from the [Google Cloud Platform Console](https://console.cloud.google.com/kubernetes/list) by the individual cluster.
+
+Look for the **Connect** button in the clusters list page.
+
+**Or**
+
+Use the command below, filling in your cluster's informtion:
+
+```
+gcloud container clusters get-credentials <cluster-name> --zone <zone> --project <project-id>
+```
+
+## Connect to EKS cluster
+
+For the most up to date instructions, follow the Amazon EKS documentation on [connecting to a cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#eks-configure-kubectl).
+
+## Connect to local minikube cluster
+
+If you are doing local development, you can use `minikube` as your
+local cluster. If `kubectl cluster-info` is not showing `minikube` as the current
+cluster, use `kubectl config set-cluster minikube` to set the active cluster.
diff --git a/doc/install/kubernetes/preparation/eks.md b/doc/install/kubernetes/preparation/eks.md
new file mode 100644
index 00000000000..c40177c4302
--- /dev/null
+++ b/doc/install/kubernetes/preparation/eks.md
@@ -0,0 +1,44 @@
+# Running GitLab on EKS
+
+There are a few nuances to Amazon EKS which are important to be aware of, when deploying GitLab.
+
+## Persistent volume management
+
+There are two methods to manage volume claims on Kubernetes:
+1. Manually creating each persistent volume (recommended on EKS)
+1. Utilizing dynamic provisioning to automatically create the persistent volumes
+
+### Manual provisioning of volumes (Recommended)
+
+Manually creating the volumes allows you to control the zone of each volume, as well as all other details supported by the underlying storage.
+
+Follow our documentation on [manually creating persistent volumes](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md#manually-creating-static-volumes).
+
+### Dynamic provisioning of volumes
+
+Dynamic provisioning utilizes a Kubernetes provisioner, like `aws-ebs`, to automatically create persistent volumes to fulfill each claim.
+
+With EKS, there are a few important details to keep in mind:
+
+1. Clusters are required to span multiple AZ's
+1. Kubernetes volume provisioners create volumes across zones without regard to which pod they belong to. This leads to scenarios where a pod with multiple volumes being unable to start due to the volumes being in different zones.
+1. There is no default Storage Class.
+
+The easiest way to solve this and still utilize dynamic provisioning is to utilize, or create, a Storage Class that is locked to a specific zone.
+
+> **Note**: Restricting volumes to specific zone will cause GitLab and any other application using this Storage Class to only reside in that zone. For multiple zone support, utilize [manually provisioned volumes](#manual-provisioning-of-volumes).
+
+To create the storage class, download and edit Amazon EKS's [sample Storage Class](https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html) and add the following parameter:
+
+```yaml
+parameters:
+ zone: <desired-zone>
+```
+
+Then [specify the Storage Class](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md#using-a-custom-storage-class) name when deploying GitLab.
+
+## External access to GitLab
+
+By default, GitLab will an deploy an ingress which will create an associated Elastic Load Balancer. Since the DNS names of ELB's cannot be known ahead of time, it is difficult to utilize Let's Encrypt to automatically provision HTTPS certificates.
+
+We recommend [using your own certificates](https://gitlab.com/charts/gitlab/blob/master/doc/installation/tls.md#option-2-use-your-own-wildcard-certificate), and then mapping your desired DNS name to the created ELB using a CNAME record.
diff --git a/doc/install/kubernetes/preparation/networking.md b/doc/install/kubernetes/preparation/networking.md
new file mode 100644
index 00000000000..b157cf31aa9
--- /dev/null
+++ b/doc/install/kubernetes/preparation/networking.md
@@ -0,0 +1,36 @@
+# Networking Prerequisites
+
+> **Note**: Amazon EKS utilizes Elastic Load Balancers, which are addressed by DNS name and cannot be known ahead of time. Skip this section.
+
+The `gitlab` chart configures a GitLab server and Kubernetes cluster which can support dynamic [Review Apps](https://docs.gitlab.com/ee/ci/review_apps/index.html), as well as services like the integrated [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html).
+
+To support the GitLab services and dynamic environments, a wildcard DNS entry is required which resolves to the external IP.
+
+## External IP
+
+To provision an external IP on GCP and Azure, simply request a new address from the Networking section. Ensure that the region matches the region your container cluster is created in. Note, it is important that the IP is not assigned at this point in time. It will be automatically assigned once the Helm chart is installed, to the Load Balancer.
+
+Set `global.hosts.externalIP` to this IP address when [deploying GitLab](../gitlab_chart.md#configuring-and-installing-gitlab).
+
+Then, create a [wildcard DNS record](#wildcard-dns-entry) which resolves to this IP address.
+
+### Creating an external IP on GCP
+
+When creating the external IP, it is critical to create it in the same region as your cluster. Otherwise, the IP address will fail to bind to the Load Balancer.
+
+1. Open the [web console](https://console.cloud.google.com)
+1. In the sidebar, browse to `VPC Network > External IP addresses`
+1. Click `Reserve static address`
+1. Choose `Regional` and select the region of your cluster
+1. Leave `Attached to` blank, as it will be automatically assigned during deployment
+
+## Wildcard DNS entry
+
+Now that an external IP address has been allocated, ensure that the wildcard DNS entry you would like to use resolves to this IP. Typically this would be an `A record` for `*`, resolving to the external IP above.
+
+Please consult the documentation for your DNS service for more information on creating DNS records:
+
+* [Google Domains](https://support.google.com/domains/answer/3290350?hl=en)
+* [GoDaddy](https://www.godaddy.com/help/add-an-a-record-19238)
+
+Set `global.hosts.domain` to this DNS name when [deploying GitLab](../gitlab_chart.md#configuring-and-installing-gitlab).
diff --git a/doc/install/kubernetes/preparation/rbac.md b/doc/install/kubernetes/preparation/rbac.md
new file mode 100644
index 00000000000..240893526d3
--- /dev/null
+++ b/doc/install/kubernetes/preparation/rbac.md
@@ -0,0 +1,16 @@
+# Role Based Access Control
+
+Until Kubernetes 1.7, there were no permissions within a cluster. With the launch of 1.7, there is now a role based access control system ([RBAC](https://kubernetes.io/docs/admin/authorization/rbac/)) which determines what services can perform actions within a cluster.
+
+RBAC affects a few different aspects of GitLab:
+* [Installation of GitLab using Helm](tiller.md#preparing-for-helm-with-rbac)
+* Prometheus monitoring
+* GitLab Runner
+
+## Checking that RBAC is enabled
+
+Try listing the current cluster roles, if it fails then `RBAC` is disabled
+
+This command will output `false` if `RBAC` is disabled and `true` otherwise
+
+`kubectl get clusterroles > /dev/null 2>&1 && echo true || echo false`
diff --git a/doc/install/kubernetes/preparation/tiller.md b/doc/install/kubernetes/preparation/tiller.md
new file mode 100644
index 00000000000..c92f8258e41
--- /dev/null
+++ b/doc/install/kubernetes/preparation/tiller.md
@@ -0,0 +1,94 @@
+# Configuring and initializing Helm Tiller
+
+To make use of Helm, you must have a [Kubernetes][k8s-io] cluster. Ensure you can access your cluster using `kubectl`.
+
+Helm consists of two parts, the `helm` client and a `tiller` server inside Kubernetes.
+
+> **Note**: If you are not able to run tiller in your cluster, for example on OpenShift, it is possible to use [tiller locally](#local-tiller) and avoid deploying it into the cluster. This should only be used when Tiller cannot be normally deployed.
+
+## Initialize Helm and Tiller
+
+Tiller is deployed into the cluster and interacts with the Kubernetes API to deploy your applications. If role based access control (RBAC) is enabled, Tiller will need to be [granted permissions](#preparing-for-helm-with-rbac) to allow it to talk to the Kubernetes API.
+
+If RBAC is not enabled, skip to [initalizing Helm](#initialize-helm).
+
+If you are not sure whether RBAC is enabled in your cluster, or to learn more, read through our [RBAC documentation](rbac.md).
+
+## Preparing for Helm with RBAC
+
+Helm's Tiller will need to be granted permissions to perform operations. These instructions grant cluster wide permissions, however for more advanced deployments [permissions can be restricted to a single namespace](https://docs.helm.sh/using_helm/#example-deploy-tiller-in-a-namespace-restricted-to-deploying-resources-only-in-that-namespace). To grant access to the cluster, we will create a new `tiller` service account and bind it to the `cluster-admin` role.
+
+Create a file `rbac-config.yaml` with the following contents:
+
+```yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: tiller
+ namespace: kube-system
+---
+apiVersion: rbac.authorization.k8s.io/v1beta1
+kind: ClusterRoleBinding
+metadata:
+ name: tiller
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+subjects:
+ - kind: ServiceAccount
+ name: tiller
+ namespace: kube-system
+```
+
+Next we need to connect to the cluster and upload the RBAC config.
+
+### Upload the RBAC config
+
+Some clusters require authentication to use `kubectl` to create the Tiller roles.
+
+#### Upload the RBAC config as an admin user (GKE)
+
+For GKE, you need to grab the admin credentials:
+
+```
+gcloud container clusters describe <cluster-name> --zone <zone> --project <project-id> --format='value(masterAuth.password)'
+```
+
+This command will output the admin password. We need the password to authenticate with `kubectl` and create the role.
+
+```
+kubectl --username=admin --password=xxxxxxxxxxxxxx create -f rbac-config.yaml
+```
+
+#### Upload the RBAC config (Other clusters)
+
+For other clusters like Amazon EKS, you can directly upload the RBAC configuration.
+
+kubectl create -f rbac-config.yaml
+
+## Initialize Helm
+
+Deploy Helm Tiller with a service account
+
+```
+helm init --service-account tiller
+```
+
+If your cluster
+previously had Helm/Tiller installed, run the following to ensure that the deployed version of Tiller matches the local Helm version:
+
+```
+helm init --upgrade --service-account tiller
+```
+
+### Patching Helm Tiller for EKS
+
+Helm Tiller requires a flag to be enabled to work properly on EKS:
+
+`kubectl -n kube-system patch deployment tiller-deploy -p '{"spec": {"template": {"spec": {"automountServiceAccountToken": true}}}}'`
+
+[helm]: https://helm.sh
+[helm-using]: https://docs.helm.sh/using_helm
+[k8s-io]: https://kubernetes.io/
+[gcp-k8s]: https://console.cloud.google.com/kubernetes/list
diff --git a/doc/install/kubernetes/preparation/tools_installation.md b/doc/install/kubernetes/preparation/tools_installation.md
new file mode 100644
index 00000000000..210bc2f9e58
--- /dev/null
+++ b/doc/install/kubernetes/preparation/tools_installation.md
@@ -0,0 +1,19 @@
+# Installing kubectl and Helm on your computer
+
+In order to work with the GitLab Helm charts, `kubectl` and `helm` must be installed and configured on your computer.
+
+## Installing `kubectl`
+
+`kubectl` is the Kubernetes command line tool, which can be used to deploy settings to the cluster.
+
+Follow the [official documentation](https://kubernetes.io/docs/tasks/tools/install-kubectl/) for the most up to date instructions.
+
+## Installing `helm`
+
+Helm is a package management tool for Kubernetes, and is used to deploy charts.
+
+You can get Helm from the project's [releases page](https://github.com/kubernetes/helm/releases), or follow other options under the official documentation of [Installing Helm](https://docs.helm.sh/using_helm/#installing-helm).
+
+# Next steps
+
+Once installed, proceed to the next [installation step](../gitlab_chart.md#prerequisites).
diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md
index a301d1a613c..932cd479d56 100644
--- a/doc/integration/recaptcha.md
+++ b/doc/integration/recaptcha.md
@@ -20,4 +20,21 @@ To use reCAPTCHA, first you must create a site and private key.
6. Check the `Enable reCAPTCHA` checkbox
-7. Save the configuration.
+7. Save the configuration.
+
+## Enabling reCAPTCHA for user logins via passwords
+
+By default, reCAPTCHA is only enabled for user registrations. To enable it for
+user logins via passwords, the `X-GitLab-Show-Login-Captcha` HTTP header must
+be set. For example, in NGINX, this can be done via the `proxy_set_header`
+configuration variable:
+
+```
+proxy_set_header X-GitLab-Show-Login-Captcha 1;
+```
+
+In GitLab Omnibus, this can be configured via `/etc/gitlab/gitlab.rb`:
+
+```ruby
+nginx['proxy_set_headers'] = { 'X-GitLab-Show-Login-Captcha' => 1 }
+```
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 3f49432ce93..db06efdae53 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -179,6 +179,81 @@ tell GitLab which groups are external via the `external_groups:` element:
} }
```
+## Bypass two factor authentication
+
+If you want some SAML authentication methods to count as 2FA on a per session basis, you can register them in the
+`upstream_two_factor_authn_contexts` list:
+
+**For Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
+ upstream_two_factor_authn_contexts:
+ %w(
+ urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN
+ )
+
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
+ ]
+ ```
+
+1. Save the file and [reconfigure][] GitLab for the changes to take effect.
+
+---
+
+**For installations from source:**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ - {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
+ upstream_two_factor_authn_contexts:
+ [
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport',
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS',
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN'
+ ]
+
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
+ ```
+
+1. Save the file and [restart GitLab][] for the changes ot take effect
+
+
+In addition to the changes in GitLab, make sure that your Idp is returning the
+`AuthnContext`. For example:
+
+```xml
+ <saml:AuthnStatement>
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:MediumStrongCertificateProtectedTransport</saml:AuthnContextClassRef>
+ </saml:AuthnContext>
+ </saml:AuthnStatement>
+```
+
## Customization
### `auto_sign_in_with_provider`
diff --git a/doc/topics/autodevops/img/auto_monitoring.png b/doc/topics/autodevops/img/auto_monitoring.png
index 92902e3ca72..2900e5d1877 100644
--- a/doc/topics/autodevops/img/auto_monitoring.png
+++ b/doc/topics/autodevops/img/auto_monitoring.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_choose_gke.png b/doc/topics/autodevops/img/guide_choose_gke.png
new file mode 100644
index 00000000000..6da3a7220da
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_choose_gke.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_cluster_apps.png b/doc/topics/autodevops/img/guide_cluster_apps.png
new file mode 100644
index 00000000000..33d25f2950d
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_cluster_apps.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_connect_cluster.png b/doc/topics/autodevops/img/guide_connect_cluster.png
index b856b81a1d0..703d536f37a 100644
--- a/doc/topics/autodevops/img/guide_connect_cluster.png
+++ b/doc/topics/autodevops/img/guide_connect_cluster.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_create_cluster.png b/doc/topics/autodevops/img/guide_create_cluster.png
new file mode 100644
index 00000000000..cd1d0fdd8da
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_create_cluster.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_create_project.png b/doc/topics/autodevops/img/guide_create_project.png
new file mode 100644
index 00000000000..4ed1071db03
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_create_project.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_enable_autodevops.png b/doc/topics/autodevops/img/guide_enable_autodevops.png
new file mode 100644
index 00000000000..0fc3ecca19a
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_enable_autodevops.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_environments.png b/doc/topics/autodevops/img/guide_environments.png
new file mode 100644
index 00000000000..1d8d5614e64
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_environments.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_environments_metrics.png b/doc/topics/autodevops/img/guide_environments_metrics.png
new file mode 100644
index 00000000000..f0d31f31581
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_environments_metrics.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_first_pipeline.png b/doc/topics/autodevops/img/guide_first_pipeline.png
new file mode 100644
index 00000000000..57459dcc9d9
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_first_pipeline.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_gitlab_gke_details.png b/doc/topics/autodevops/img/guide_gitlab_gke_details.png
new file mode 100644
index 00000000000..bc5a53800f7
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_gitlab_gke_details.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_gke_apis_after.png b/doc/topics/autodevops/img/guide_gke_apis_after.png
new file mode 100644
index 00000000000..380de958867
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_gke_apis_after.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_gke_apis_before.png b/doc/topics/autodevops/img/guide_gke_apis_before.png
new file mode 100644
index 00000000000..d06fc707887
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_gke_apis_before.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_google_auth.png b/doc/topics/autodevops/img/guide_google_auth.png
new file mode 100644
index 00000000000..b97b2be9f15
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_google_auth.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_google_signin.png b/doc/topics/autodevops/img/guide_google_signin.png
new file mode 100644
index 00000000000..e59fc94bd4c
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_google_signin.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_ide_commit.png b/doc/topics/autodevops/img/guide_ide_commit.png
new file mode 100644
index 00000000000..188f60f2a4b
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_ide_commit.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_integration.png b/doc/topics/autodevops/img/guide_integration.png
deleted file mode 100644
index 723b2619ea2..00000000000
--- a/doc/topics/autodevops/img/guide_integration.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_merge_request.png b/doc/topics/autodevops/img/guide_merge_request.png
new file mode 100644
index 00000000000..d78e69be776
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_merge_request.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_merge_request_ide.png b/doc/topics/autodevops/img/guide_merge_request_ide.png
new file mode 100644
index 00000000000..c825b0849e1
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_merge_request_ide.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_merge_request_review_app.png b/doc/topics/autodevops/img/guide_merge_request_review_app.png
new file mode 100644
index 00000000000..1b9b854ddac
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_merge_request_review_app.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_pipeline_stages.png b/doc/topics/autodevops/img/guide_pipeline_stages.png
new file mode 100644
index 00000000000..6e2f078152b
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_pipeline_stages.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_project_landing_page.png b/doc/topics/autodevops/img/guide_project_landing_page.png
new file mode 100644
index 00000000000..4f8d2eb10b1
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_project_landing_page.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_project_template.png b/doc/topics/autodevops/img/guide_project_template.png
new file mode 100644
index 00000000000..298ac0f6fcf
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_project_template.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_secret.png b/doc/topics/autodevops/img/guide_secret.png
deleted file mode 100644
index 01f5aa49908..00000000000
--- a/doc/topics/autodevops/img/guide_secret.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/img/rollout_staging_disabled.png b/doc/topics/autodevops/img/rollout_staging_disabled.png
index 71e36b440f0..4c7c6768666 100644
--- a/doc/topics/autodevops/img/rollout_staging_disabled.png
+++ b/doc/topics/autodevops/img/rollout_staging_disabled.png
Binary files differ
diff --git a/doc/topics/autodevops/img/rollout_staging_enabled.png b/doc/topics/autodevops/img/rollout_staging_enabled.png
index d0d1d356627..f45c1c2cb37 100644
--- a/doc/topics/autodevops/img/rollout_staging_enabled.png
+++ b/doc/topics/autodevops/img/rollout_staging_enabled.png
Binary files differ
diff --git a/doc/topics/autodevops/img/staging_enabled.png b/doc/topics/autodevops/img/staging_enabled.png
index 0ef1a67d641..f0e0cd1cfcd 100644
--- a/doc/topics/autodevops/img/staging_enabled.png
+++ b/doc/topics/autodevops/img/staging_enabled.png
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 103836e59d0..1d26a743500 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -1,6 +1,6 @@
# Auto DevOps
-> [Introduced][ce-37115] in GitLab 10.0.
+> [Introduced][ce-37115] in GitLab 10.0. Generally available on GitLab 11.0.
Auto DevOps automatically detects, builds, tests, deploys, and monitors your
applications.
@@ -13,6 +13,12 @@ without needing to configure anything. Just push your code and GitLab takes
care of everything else. This makes it easier to start new projects and brings
consistency to how applications are set up throughout a company.
+## Quick start
+
+If you are using GitLab.com, see the [quick start guide](quick_start_guide.md)
+for using Auto DevOps with GitLab.com and a Kubernetes cluster on Google Kubernetes
+Engine.
+
## Comparison to application platforms and PaaS
Auto DevOps provides functionality described by others as an application
@@ -34,19 +40,19 @@ in a couple of ways:
## Features
Comprised of a set of stages, Auto DevOps brings these best practices to your
-project in an easy and automatic way:
+project in a simple and automatic way:
1. [Auto Build](#auto-build)
1. [Auto Test](#auto-test)
-1. [Auto Code Quality](#auto-code-quality)
-1. [Auto SAST (Static Application Security Testing)](#auto-sast)
-1. [Auto Dependency Scanning](#auto-dependency-scanning)
-1. [Auto License Management](#auto-license-management)
+1. [Auto Code Quality](#auto-code-quality) **[STARTER]**
+1. [Auto SAST (Static Application Security Testing)](#auto-sast) **[ULTIMATE]**
+1. [Auto Dependency Scanning](#auto-dependency-scanning) **[ULTIMATE]**
+1. [Auto License Management](#auto-license-management) **[ULTIMATE]**
1. [Auto Container Scanning](#auto-container-scanning)
1. [Auto Review Apps](#auto-review-apps)
-1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast)
+1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast) **[ULTIMATE]**
1. [Auto Deploy](#auto-deploy)
-1. [Auto Browser Performance Testing](#auto-browser-performance-testing)
+1. [Auto Browser Performance Testing](#auto-browser-performance-testing) **[PREMIUM]**
1. [Auto Monitoring](#auto-monitoring)
As Auto DevOps relies on many different components, it's good to have a basic
@@ -135,10 +141,9 @@ and `1.2.3.4` is the IP address of your load balancer; generally NGINX
([see requirements](#requirements)). How to set up the DNS record is beyond
the scope of this document; you should check with your DNS provider.
-Alternatively you can use free public services like [nip.io](http://nip.io) or
-[nip.io](http://nip.io) which provide automatic wildcard DNS without any
-configuration. Just set the Auto DevOps base domain to `1.2.3.4.nip.io` or
-`1.2.3.4.nip.io`.
+Alternatively you can use free public services like [nip.io](http://nip.io)
+which provide automatic wildcard DNS without any configuration. Just set the
+Auto DevOps base domain to `1.2.3.4.nip.io`.
Once set up, all requests will hit the load balancer, which in turn will route
them to the Kubernetes pods that run your application(s).
@@ -198,12 +203,6 @@ and verifying that your app is deployed as a review app in the Kubernetes
cluster with the `review/*` environment scope. Similarly, you can check the
other environments.
-## Quick start
-
-If you are using GitLab.com, see our [quick start guide](quick_start_guide.md)
-for using Auto DevOps with GitLab.com and an external Kubernetes cluster on
-Google Cloud.
-
## Enabling Auto DevOps
If you haven't done already, read the [requirements](#requirements) to make
@@ -288,7 +287,7 @@ NOTE: **Note:**
Auto Test uses tests you already have in your application. If there are no
tests, it's up to you to add them.
-### Auto Code Quality
+### Auto Code Quality **[STARTER]**
Auto Code Quality uses the
[Code Quality image](https://gitlab.com/gitlab-org/security-products/codequality) to run
@@ -323,7 +322,7 @@ to run analysis on the project dependencies and checks for potential security is
report is created, it's uploaded as an artifact which you can later download and
check out.
-In GitLab Ultimate, any security warnings are also
+Any security warnings are also
[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dependency_scanning.html).
### Auto License Management **[ULTIMATE]**
@@ -331,12 +330,12 @@ In GitLab Ultimate, any security warnings are also
> Introduced in [GitLab Ultimate][ee] 11.0.
License Management uses the
-[License Management Docker image](https://gitlab.com/gitlab-org/security-products/license_management)
+[License Management Docker image](https://gitlab.com/gitlab-org/security-products/license-management)
to search the project dependencies for their license. Once the
report is created, it's uploaded as an artifact which you can later download and
check out.
-In GitLab Ultimate, any licenses are also
+Any licenses are also
[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/license_management.html).
### Auto Container Scanning
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 61c04f3d9bb..44b0cf758dc 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -1,143 +1,290 @@
-# Auto DevOps: quick start guide
+# Getting started with Auto DevOps
-> [Introduced][ce-37115] in GitLab 10.0.
+This is a step-by-step guide that will help you use [Auto DevOps](index.md) to
+deploy a project hosted on GitLab.com to Google Kubernetes Engine.
-This is a step-by-step guide to deploying a project hosted on GitLab.com to
-Google Cloud, using Auto DevOps.
+We will use GitLab's native Kubernetes integration, so you will not need
+to create a Kubernetes cluster manually using the Google Cloud Platform console.
+We will create and deploy a simple application that we create from a GitLab template.
-We made a minimal [Ruby
-application](https://gitlab.com/auto-devops-examples/minimal-ruby-app) to use
-as an example for this guide. It contains two main files:
+These instructions will also work for a self-hosted GitLab instance; you'll just
+need to ensure your own [Runners are configured](../../ci/runners/README.md) and
+[Google OAuth is enabled](../../integration/google.md).
-* `server.rb` - our application. It will start an HTTP server on port 5000 and
- render "Hello, world!"
-* `Dockerfile` - to build our app into a container image. It will use a ruby
- base image and run `server.rb`
+## Configuring your Google account
-## Fork sample project on GitLab.com
+Before creating and connecting your Kubernetes cluster to your GitLab project,
+you need a Google Cloud Platform account. If you don't already have one,
+sign up at https://console.cloud.google.com. You'll need to either sign in with an existing
+Google account (for example, one that you use to access Gmail, Drive, etc.) or create a new one.
-Let’s start by forking our sample application. Go to [the project
-page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the
-**Fork** button. Soon you should have a project under your namespace with the
-necessary files.
+1. Follow the steps as outlined in the ["Before you begin" section of the Kubernetes Engine docs](https://cloud.google.com/kubernetes-engine/docs/quickstart#before-you-begin)
+ in order for the required APIs and related services to be enabled.
+1. Make sure you have created a [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account).
-You can also start a new project from a
-[GitLab project template](https://gitlab.com/gitlab-org/project-templates) if
-you want to use a different language.
+TIP: **Tip:**
+Every new Google Cloud Platform (GCP) account receives [$300 in credit](https://console.cloud.google.com/freetrial),
+and in partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's
+Google Kubernetes Engine Integration. All you have to do is [follow this link](https://goo.gl/AaJzRW) and apply for credit.
-## Setup your own cluster on Google Kubernetes Engine
+## Creating a new project from a template
-If you do not already have a Google Cloud account, create one at
-https://console.cloud.google.com.
+We will use one of GitLab's project templates to get started. As the name suggests,
+those projects provide a barebones application built on some well-known frameworks.
-Visit the [**Kubernetes Engine**](https://console.cloud.google.com/kubernetes/list)
-tab and create a new cluster. You can change the name and leave the rest of the
-default settings. Once you have your cluster running, you need to connect to the
-cluster by following the Google interface.
+1. In GitLab, click the plus icon (**+**) at the top of the navigation bar and select
+ **New project**.
+1. Go to the **Create from template** tab where you can choose among a Ruby on
+ Rails, Spring, or NodeJS Express project. For this example,
+ we'll use the Ruby on Rails template.
-## Connect to Kubernetes cluster
+ ![Select project template](img/guide_project_template.png)
-You need to have the Google Cloud SDK installed. e.g.
-On macOS, install [homebrew](https://brew.sh):
+1. Give your project a name, optionally a description, and make it public so that
+ you can take advantage of the features available in the
+ [GitLab Gold plan](https://about.gitlab.com/pricing/#gitlab-com).
-1. Install Brew Caskroom: `brew install caskroom/cask/brew-cask`
-2. Install Google Cloud SDK: `brew cask install google-cloud-sdk`
-3. Add `kubectl` with: `gcloud components install kubectl`
-4. Log in: `gcloud auth login`
+ ![Create project](img/guide_create_project.png)
-Now go back to the Google interface, find your cluster, follow the instructions
-under "Connect to the cluster" and open the Kubernetes Dashboard. It will look
-something like:
+1. Click **Create project**.
-```sh
-gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX
-```
+Now that the project is created, the next step is to create the Kubernetes cluster
+under which this application will be deployed.
-Finally, run `kubectl proxy`.
+## Creating a Kubernetes cluster from within GitLab
-![connect to cluster](img/guide_connect_cluster.png)
+1. On the project's landing page, click the button labeled **Add Kubernetes cluster**
+ (note that this option is also available when you navigate to **Operations > Kubernetes**).
-## Copy credentials to GitLab.com project
+ ![Project landing page](img/guide_project_landing_page.png)
-Once you have the Kubernetes Dashboard interface running, you should visit
-**Secrets** under the "Config" section. There, you should find the settings we
-need for GitLab integration: `ca.crt` and token.
+1. Choose **Create on Google Kubernetes Engine**.
-![connect to cluster](img/guide_secret.png)
+ ![Choose GKE](img/guide_choose_gke.png)
-You need to copy-paste the `ca.crt` and token into your project on GitLab.com in
-the Kubernetes integration page under project
-**Settings > Integrations > Project services > Kubernetes**. Don't actually copy
-the namespace though. Each project should have a unique namespace, and by leaving
-it blank, GitLab will create one for you.
+1. Sign in with Google.
-![connect to cluster](img/guide_integration.png)
+ ![Google sign in](img/guide_google_signin.png)
-For the API URL, you should use the "Endpoint" IP from your cluster page on
-Google Cloud Platform.
+1. Connect with your Google account and press **Allow** when asked (this will
+ be shown only the first time you connect GitLab with your Google account).
-## Expose application to the world
+ ![Google auth](img/guide_google_auth.png)
-In order to be able to visit your application, you need to install an NGINX
-ingress controller and point your domain name to its external IP address. Let's
-see how that's done.
+1. The last step is to fill in the cluster details. Give it a name, leave the
+ environment scope as is, and choose the GCP project under which the cluster
+ will be created. (Per the instructions when you
+ [configured your Google account](#configuring-your-google-account), a project
+ should have already been created for you.) Next, choose the
+ [region/zone](https://cloud.google.com/compute/docs/regions-zones/) under which the
+ cluster will be created, enter the number of nodes you want it to have, and
+ finally choose their [machine type](https://cloud.google.com/compute/docs/machine-types).
-### Set up Ingress controller
+ ![GitLab GKE cluster details](img/guide_gitlab_gke_details.png)
-You’ll need to make sure you have an ingress controller. If you don’t have one, do:
+1. Once ready, click **Create Kubernetes cluster**.
-```sh
-brew install kubernetes-helm
-helm init
-helm install --name ruby-app stable/nginx-ingress
-```
+After a couple of minutes, the cluster will be created. You can also see its
+status on your [GCP dashboard](https://console.cloud.google.com/kubernetes).
-This should create several services including `ruby-app-nginx-ingress-controller`.
-You can list your services by running `kubectl get svc` to confirm that.
+The next step is to install some applications on your cluster that are needed
+to take full advantage of Auto DevOps.
-### Point DNS at Cluster IP
+## Installing Helm, Ingress, and Prometheus
-Find out the external IP address of the `ruby-app-nginx-ingress-controller` by
-running:
+GitLab's Kubernetes integration comes with some
+[pre-defined applications](../../user/project/clusters/index.md#installing-applications)
+for you to install.
-```sh
-kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
-```
+![Cluster applications](img/guide_cluster_apps.png)
+
+The first one to install is Helm Tiller, a package manager for Kubernetes, which
+is needed in order to install the rest of the applications. Go ahead and click
+its **Install** button.
+
+Once it's installed, the other applications that rely on it will each have their **Install**
+button enabled. For this guide, we need Ingress and Prometheus. Ingress provides
+load balancing, SSL termination, and name-based virtual hosting, using NGINX behind
+the scenes. Prometheus is an open-source monitoring and alerting system that we'll
+use to supervise the deployed application. We will not install GitLab Runner as
+we'll use the shared Runners that GitLab.com provides.
+
+After the Ingress is installed, wait a few seconds and copy the IP address that
+is displayed, which we'll use in the next step when enabling Auto DevOps.
+
+## Enabling Auto DevOps
+
+Now that the Kubernetes cluster is set up and ready, let's enable Auto DevOps.
+
+1. First, navigate to **Settings > CI/CD > Auto DevOps**.
+1. Select **Enable Auto DevOps**.
+1. Add in your base **Domain** by using the one GitLab suggests. Note that
+ generally, you would associate the IP address with a domain name on your
+ registrar's settings. In this case, for the sake of the guide, we will use
+ an alternative DNS that will map any domain name of the scheme
+ `anything.ip_address.nip.io` to the corresponding `ip_address`. For example,
+ if the IP address of the Ingress is `1.2.3.4`, the domain name to fill in
+ would be `1.2.3.4.nip.io`.
+1. Lastly, let's select the [continuous deployment strategy](index.md#deployment-strategy)
+ which will automatically deploy the application to production once the pipeline
+ successfully runs on the `master` branch.
+1. Click **Save changes**.
+
+ ![Auto DevOps settings](img/guide_enable_autodevops.png)
+
+Once you complete all the above and save your changes, a new pipeline is
+automatically created. To view the pipeline, go to **CI/CD > Pipelines**.
+
+![First pipeline](img/guide_first_pipeline.png)
+
+In the next section we'll break down the pipeline and explain what each job does.
+
+## Deploying the application
+
+By now you should see the pipeline running, but what is it running exactly?
+
+To navigate inside the pipeline, click its status badge. (It's status should be "running").
+The pipeline is split into 4 stages, each running a couple of jobs.
+
+![Pipeline stages](img/guide_pipeline_stages.png)
+
+In the **build** stage, the application is built into a Docker image and then
+uploaded to your project's [Container Registry](../../user/project/container_registry.md) ([Auto Build](index.md#auto-build)).
+
+In the **test** stage, GitLab runs various checks on the application:
+
+- The `test` job runs unit and integration tests by detecting the language and
+ framework ([Auto Test](index.md#auto-test))
+- The `code_quality` job checks the code quality and is allowed to fail
+ ([Auto Code Quality](index.md#auto-code-quality)) **[STARTER]**
+- The `container_scanning` job checks the Docker container if it has any
+ vulnerabilities and is allowed to fail ([Auto Container Scanning](index.md#auto-container-scanning))
+- The `dependency_scanning` job checks if the application has any dependencies
+ susceptible to vulnerabilities and is allowed to fail ([Auto Dependency Scanning](index.md#auto-dependency-scanning)) **[ULTIMATE]**
+- The `sast` job runs static analysis on the current code to check for potential
+ security issues and is allowed to fail([Auto SAST](index.md#auto-sast)) **[ULTIMATE]**
+- The `license_management` job searches the application's dependencies to determine each of their
+ licenses and is allowed to fail ([Auto License Management](index.md#auto-license-management)) **[ULTIMATE]**
NOTE: **Note:**
-If your ingress controller has been installed in a different way, you can find
-how to get the external IP address in the
-[Cluster documentation](../../user/project/clusters/index.md#getting-the-external-ip-address).
+As you might have noticed, all jobs except `test` are allowed to fail in the
+test stage.
+
+The **production** stage is run after the tests and checks finish, and it automatically
+deploys the application in Kubernetes ([Auto Deploy](index.md#auto-deploy)).
+
+Lastly, in the **performance** stage, some performance tests will run
+on the deployed application
+([Auto Browser Performance Testing](index.md#auto-browser-performance-testing)). **[PREMIUM]**
+
+---
-Use this IP address to configure your DNS. This part heavily depends on your
-preferences and domain provider. But in case you are not sure, just create an
-A record with a wildcard host like `*.<your-domain>`.
+The URL for the deployed application can be found under the **Environments**
+page where you can also monitor your application. Let's explore that.
+
+### Monitoring
+
+Now that the application is successfully deployed, let's navigate to its
+website. First, go to **Operations > Environments**.
+
+![Environments](img/guide_environments.png)
+
+In **Environments** you can see some details about the deployed
+applications. In the rightmost column for the production environment, you can make use of the three icons:
+
+- The first icon will open the URL of the application that is deployed in
+ production. It's a very simple page, but the important part is that it works!
+- The next icon with the small graph will take you to the metrics page where
+ Prometheus collects data about the Kubernetes cluster and how the application
+ affects it (in terms of memory/CPU usage, latency, etc.).
+
+ ![Environments metrics](img/guide_environments_metrics.png)
+
+- The third icon is the [web terminal](../../ci/environments.md#web-terminals)
+ and it will open a terminal session right inside the container where the
+ application is running.
+
+Right below, there is the
+[Deploy Board](https://docs.gitlab.com/ee/user/project/deploy_boards.md).
+The squares represent pods in your Kubernetes cluster that are associated with
+the given environment. Hovering above each square you can see the state of a
+deployment and clicking a square will take you to the pod's logs page.
+
+TIP: **Tip:**
+There is only one pod hosting the application at the moment, but you can add
+more pods by defining the [`REPLICAS` variable](index.md#environment-variables)
+under **Settings > CI/CD > Variables**.
+
+### Working with branches
+
+Following the [GitLab flow](../../workflow/gitlab_flow.md#working-with-feature-branches)
+let's create a feature branch that will add some content to the application.
+
+Under your repository, navigate to the following file: `app/views/welcome/index.html.erb`.
+By now, it should only contain a paragraph: `<p>You're on Rails!</p>`, so let's
+start adding content. Let's use GitLab's [Web IDE](../../user/project/web_ide/index.md) to make the change. Once
+you're on the Web IDE, make the following change:
+
+```html
+<p>You're on Rails! Powered by GitLab Auto DevOps.</p>
+```
+
+Stage the file, add a commit message, and create a new branch and a merge request
+by clicking **Commit**.
+
+![Web IDE commit](img/guide_ide_commit.png)
+
+Once you submit the merge request, you'll see the pipeline running. This will
+run all the jobs as [described previously](#deploying-the-application), as well
+a few more that run only on branches other than `master`.
+
+![Merge request](img/guide_merge_request.png)
+
+After a few minutes you'll notice that there was a failure in a test.
+This means there's a test that was 'broken' by our change.
+Navigating into the `test` job that failed, you can see what the broken test is:
+
+```
+Failure:
+WelcomeControllerTest#test_should_get_index [/app/test/controllers/welcome_controller_test.rb:7]:
+<You're on Rails!> expected but was
+<You're on Rails! Powered by GitLab Auto DevOps.>..
+Expected 0 to be >= 1.
+
+bin/rails test test/controllers/welcome_controller_test.rb:4
+```
-Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is
-assigned to the cluster IP.
+Let's fix that:
-## Set up Auto DevOps
+1. Back to the merge request, click the **Web IDE** button.
+1. Find the `test/controllers/welcome_controller_test.rb` file and open it.
+1. Change line 7 to say `You're on Rails! Powered by GitLab Auto DevOps.`
+1. Click **Commit**.
+1. On your left, under "Unstaged changes", click the little checkmark icon
+ to stage the changes.
+1. Write a commit message and click **Commit**.
-In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps
-section. Select "Enable Auto DevOps", add in your base domain, and save.
+Now, if you go back to the merge request you should not only see the test passing,
+but also the application deployed as a [review app](index.md#auto-review-apps). You
+can visit it by following the URL in the merge request. The changes that we
+previously made should be there.
-Next, a pipeline needs to be triggered. Since the test project doesn't have a
-`.gitlab-ci.yml`, you need to either push a change to the repository or
-manually visit `https://gitlab.com/<username>/minimal-ruby-app/pipelines/new`,
-where `<username>` is your username.
+![Review app](img/guide_merge_request_review_app.png)
-This will create a new pipeline with several jobs: `build`, `test`, `code_quality`,
-and `production`. The `build` job will create a Docker image with your new
-change and push it to the Container Registry. The `test` job will test your
-changes, whereas the `code_quality` job will run static analysis on your changes.
-Finally, the `production` job will deploy your changes to a production application.
+Once you merge the merge request, the pipeline will run on the `master` branch,
+and the application will be eventually deployed straight to production.
-Once the deploy job succeeds you should be able to see your application by
-visiting the Kubernetes dashboard. Select the namespace of your project, which
-will look like `minimal-ruby-app-23`, but with a unique ID for your project,
-and your app will be listed as "production" under the Deployment tab.
+## Conclusion
-Once its ready, just visit `http://minimal-ruby-app.example.com` to see the
-famous "Hello, world!"!
+After implementing this project, you should now have a solid understanding of the basics of Auto DevOps.
+We started from building and testing to deploying and monitoring an application
+all within GitLab. Despite its automatic nature, Audo DevOps can also be configured
+and customized to fit your workflow. Here are some helpful resources for further reading:
-[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
+1. [Auto DevOps](index.md)
+1. [Multiple Kubernetes clusters](index.md#using-multiple-kubernetes-clusters) **[PREMIUM]**
+1. [Incremental rollout to production](index.md#incremental-rollout-to-production) **[PREMIUM]**
+1. [Disable jobs you don't need with environment variables](index.md#environment-variables)
+1. [Use a static IP for your cluster](../../user/project/clusters/index.md#using-a-static-ip)
+1. [Use your own buildpacks to build your application](index.md#custom-buildpacks)
+1. [Prometheus monitoring](../../user/project/integrations/prometheus.md)
diff --git a/doc/update/10.8-to-11.0.md b/doc/update/10.8-to-11.0.md
index f9b6044bd2f..22a0c9f950c 100644
--- a/doc/update/10.8-to-11.0.md
+++ b/doc/update/10.8-to-11.0.md
@@ -4,10 +4,9 @@ comments: false
# From 10.8 to 11.0
-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).
+Make sure you view this update guide from the branch (version) of GitLab you would
+like to install (e.g., `11-0-stable`. You can select the branch 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
diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md
index 26329f20339..9801a0a14ed 100644
--- a/doc/user/admin_area/settings/sign_up_restrictions.md
+++ b/doc/user/admin_area/settings/sign_up_restrictions.md
@@ -38,3 +38,4 @@ semicolon, comma, or a new line.
![Domain Blacklist](img/domain_blacklist.png)
[ce-5259]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5259
+[ce-598]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/598
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 58a483bb3b2..20c46cafbe5 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -7,9 +7,10 @@ cluster in a few steps.
## Overview
-With a Kubernetes cluster associated to your project, you can use
+With one or more Kubernetes clusters associated to your project, you can use
[Review Apps](../../../ci/review_apps/index.md), deploy your applications, run
-your pipelines, and much more, in an easy way.
+your pipelines, use it with [Auto DevOps](../../../topics/autodevops/index.md),
+and much more, all from within GitLab.
There are two options when adding a new cluster to your project; either associate
your account with Google Kubernetes Engine (GKE) so that you can [create new
@@ -18,59 +19,65 @@ or provide the credentials to an [existing Kubernetes cluster](#adding-an-existi
## Adding and creating a new GKE cluster via GitLab
+TIP: **Tip:**
+Every new Google Cloud Platform (GCP) account receives [$300 in credit upon sign up](https://console.cloud.google.com/freetrial),
+and in partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's
+Google Kubernetes Engine Integration. All you have to do is [follow this link](https://goo.gl/AaJzRW) and apply for credit.
+
NOTE: **Note:**
-You need Maintainer [permissions] and above to access the Kubernetes page.
-
-Before proceeding, make sure the following requirements are met:
-
-- The [Google authentication integration](../../../integration/google.md) must
- be enabled in GitLab at the instance level. If that's not the case, ask your
- GitLab administrator to enable it.
-- Your associated Google account must have the right privileges to manage
- clusters on GKE. That would mean that a [billing
- account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
- must be set up and that you have to have permissions to access it.
-- You must have Maintainer [permissions] in order to be able to access the
- **Kubernetes** page.
-- You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled
-- You must have [Resource Manager
- API](https://cloud.google.com/resource-manager/)
+The [Google authentication integration](../../../integration/google.md) must
+be enabled in GitLab at the instance level. If that's not the case, ask your
+GitLab administrator to enable it. On GitLab.com, this is enabled.
+
+### Requirements
+
+Before creating your first cluster on Google Kubernetes Engine with GitLab's
+integration, make sure the following requirements are met:
+
+- A [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
+ is set up and you have permissions to access it.
+- The Kubernetes Engine API is enabled. Follow the steps as outlined in the
+ ["Before you begin" section of the Kubernetes Engine docs](https://cloud.google.com/kubernetes-engine/docs/quickstart#before-you-begin).
+
+### Creating the cluster
If all of the above requirements are met, you can proceed to create and add a
-new Kubernetes cluster that will be hosted on GKE to your project:
+new Kubernetes cluster to your project:
1. Navigate to your project's **Operations > Kubernetes** page.
+
+ NOTE: **Note:**
+ You need Maintainer [permissions] and above to access the Kubernetes page.
+
1. Click on **Add Kubernetes cluster**.
1. Click on **Create with Google Kubernetes Engine**.
1. Connect your Google account if you haven't done already by clicking the
**Sign in with Google** button.
-1. Fill in the requested values:
+1. From there on, choose your cluster's settings:
- **Kubernetes cluster name** - The name you wish to give the cluster.
- **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster.
- - **Google Cloud Platform project** - The project you created in your GCP
- console that will host the Kubernetes cluster. This must **not** be confused
- with the project ID. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
- - **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/)
+ - **Google Cloud Platform project** - Choose the project you created in your GCP
+ console that will host the Kubernetes cluster. Learn more about
+ [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
+ - **Zone** - Choose the [region zone](https://cloud.google.com/compute/docs/regions-zones/)
under which the cluster will be created.
- - **Number of nodes** - The number of nodes you wish the cluster to have.
+ - **Number of nodes** - Enter the number of nodes you wish the cluster to have.
- **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types)
of the Virtual Machine instance that the cluster will be based on.
1. Finally, click the **Create Kubernetes cluster** button.
-After a few moments, your cluster should be created. If something goes wrong,
-you will be notified.
-
-You can now proceed to install some pre-defined applications and then
-enable the Cluster integration.
+After a couple of minutes, your cluster will be ready to go. You can now proceed
+to install some [pre-defined applications](#installing-applications).
## Adding an existing Kubernetes cluster
-NOTE: **Note:**
-You need Maintainer [permissions] and above to access the Kubernetes page.
-
To add an existing Kubernetes cluster to your project:
1. Navigate to your project's **Operations > Kubernetes** page.
+
+ NOTE: **Note:**
+ You need Maintainer [permissions] and above to access the Kubernetes page.
+
1. Click on **Add Kubernetes cluster**.
1. Click on **Add an existing Kubernetes cluster** and fill in the details:
- **Kubernetes cluster name** (required) - The name you wish to give the cluster.
@@ -91,9 +98,8 @@ To add an existing Kubernetes cluster to your project:
to create one. You can also view or create service tokens in the
[Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config)
(under **Config > Secrets**).
- - **Project namespace** (optional) - The following apply:
- - By default you don't have to fill it in; by leaving it blank, GitLab will
- create one for you.
+ - **Project namespace** (optional) - You don't have to fill it in; by leaving
+ it blank, GitLab will create one for you. Also:
- Each project should have a unique namespace.
- The project namespace is not necessarily the namespace of the secret, if
you're using a secret with broader permissions, like the secret from `default`.
@@ -103,11 +109,8 @@ To add an existing Kubernetes cluster to your project:
be the same.
1. Finally, click the **Create Kubernetes cluster** button.
-After a few moments, your cluster should be created. If something goes wrong,
-you will be notified.
-
-You can now proceed to install some pre-defined applications and then
-enable the Kubernetes cluster integration.
+After a couple of minutes, your cluster will be ready to go. You can now proceed
+to install some [pre-defined applications](#installing-applications).
## Security implications
@@ -152,9 +155,9 @@ added directly to your configured cluster. Those applications are needed for
| Application | GitLab version | Description |
| ----------- | :------------: | ----------- |
-| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. |
+| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. |
-| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications |
+| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. |
| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. |
diff --git a/doc/user/project/img/group_issue_board.png b/doc/user/project/img/group_issue_board.png
new file mode 100644
index 00000000000..be360d18540
--- /dev/null
+++ b/doc/user/project/img/group_issue_board.png
Binary files differ
diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png
index 5f6dc9e4e8b..50e051e25a0 100644
--- a/doc/user/project/img/issue_board.png
+++ b/doc/user/project/img/issue_board.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png
index 973d9f7cde4..91098daa1d1 100644
--- a/doc/user/project/img/issue_board_add_list.png
+++ b/doc/user/project/img/issue_board_add_list.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_assignee_lists.png b/doc/user/project/img/issue_board_assignee_lists.png
new file mode 100644
index 00000000000..1ec94d22e33
--- /dev/null
+++ b/doc/user/project/img/issue_board_assignee_lists.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_creation.png b/doc/user/project/img/issue_board_creation.png
new file mode 100644
index 00000000000..9dc4925b0a5
--- /dev/null
+++ b/doc/user/project/img/issue_board_creation.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_edit_button.png b/doc/user/project/img/issue_board_edit_button.png
new file mode 100644
index 00000000000..23883175344
--- /dev/null
+++ b/doc/user/project/img/issue_board_edit_button.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_focus_mode.gif b/doc/user/project/img/issue_board_focus_mode.gif
new file mode 100644
index 00000000000..9565bdb0865
--- /dev/null
+++ b/doc/user/project/img/issue_board_focus_mode.gif
Binary files differ
diff --git a/doc/user/project/img/issue_board_move_issue_card_list.png b/doc/user/project/img/issue_board_move_issue_card_list.png
index 3666dbb87ab..cce252234c1 100644
--- a/doc/user/project/img/issue_board_move_issue_card_list.png
+++ b/doc/user/project/img/issue_board_move_issue_card_list.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_system_notes.png b/doc/user/project/img/issue_board_system_notes.png
index bd0f5f54095..c6ecb498198 100644
--- a/doc/user/project/img/issue_board_system_notes.png
+++ b/doc/user/project/img/issue_board_system_notes.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_view_scope.png b/doc/user/project/img/issue_board_view_scope.png
new file mode 100644
index 00000000000..4e03cecbc2d
--- /dev/null
+++ b/doc/user/project/img/issue_board_view_scope.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png
index 127b9b08cc7..357dff42488 100644
--- a/doc/user/project/img/issue_board_welcome_message.png
+++ b/doc/user/project/img/issue_board_welcome_message.png
Binary files differ
diff --git a/doc/user/project/img/issue_boards_add_issues_modal.png b/doc/user/project/img/issue_boards_add_issues_modal.png
index bedaf724a15..625a4304eaf 100644
--- a/doc/user/project/img/issue_boards_add_issues_modal.png
+++ b/doc/user/project/img/issue_boards_add_issues_modal.png
Binary files differ
diff --git a/doc/user/project/img/issue_boards_multiple.png b/doc/user/project/img/issue_boards_multiple.png
new file mode 100644
index 00000000000..4b2b8d457f1
--- /dev/null
+++ b/doc/user/project/img/issue_boards_multiple.png
Binary files differ
diff --git a/doc/user/project/img/issue_boards_remove_issue.png b/doc/user/project/img/issue_boards_remove_issue.png
index 8b3beca97cf..9a2fad2cc7f 100644
--- a/doc/user/project/img/issue_boards_remove_issue.png
+++ b/doc/user/project/img/issue_boards_remove_issue.png
Binary files differ
diff --git a/doc/user/project/import/bitbucket.md b/doc/user/project/import/bitbucket.md
index b22c7db0047..e3d625cc621 100644
--- a/doc/user/project/import/bitbucket.md
+++ b/doc/user/project/import/bitbucket.md
@@ -9,6 +9,10 @@ The [Bitbucket integration][bb-import] must be first enabled in order to be
able to import your projects from Bitbucket. Ask your GitLab administrator
to enable this if not already.
+>**Note:**
+The BitBucket importer currently only works with BitBucket's cloud offering
+(bitbucket.org) and does not work with BitBucket Server (aka Stash).
+
- At its current state, the Bitbucket importer can import:
- the repository description (GitLab 7.7+)
- the Git repository data (GitLab 7.7+)
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 9ca1e6226c5..10647e33f4c 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -1,16 +1,10 @@
-# Issue Board
+# Issue Boards
->**Note:**
-[Introduced][ce-5554] in [GitLab 8.11](https://about.gitlab.com/2016/08/22/gitlab-8-11-released/#issue-board).
+> [Introduced][ce-5554] in [GitLab 8.11](https://about.gitlab.com/2016/08/22/gitlab-8-11-released/#issue-board).
The GitLab Issue Board is a software project management tool used to plan,
organize, and visualize a workflow for a feature or product release.
-It can be seen like a light version of a [Kanban] or a [Scrum] board.
-
-Other interesting links:
-
-- [GitLab Issue Board landing page on about.gitlab.com][landing]
-- [YouTube video introduction to Issue Boards][youtube]
+It can be used as a [Kanban] or a [Scrum] board.
![GitLab Issue Board](img/issue_board.png)
@@ -18,7 +12,7 @@ Other interesting links:
The Issue Board builds on GitLab's existing
[issue tracking functionality](issues/index.md#issue-tracker) and
-leverages the power of [labels] by utilizing them as lists of the scrum board.
+leverages the power of [labels](labels.md) by utilizing them as lists of the scrum board.
With the Issue Board you can have a different view of your issues while
maintaining the same filtering and sorting abilities you see across the
@@ -33,15 +27,23 @@ You create issues, host code, perform reviews, build, test,
and deploy from one single platform. Issue Boards help you to visualize
and manage the entire process _in_ GitLab.
-With [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards), available
-only in [GitLab Ultimate](https://about.gitlab.com/products/),
+With [Multiple Issue Boards](#multiple-issue-boards), available
+only in [GitLab Enterprise Edition](#features-per-tier),
you go even further, as you can not only keep yourself and your project
organized from a broader perspective with one Issue Board per project,
but also allow your team members to organize their own workflow by creating
multiple Issue Boards within the same project.
+For a visual overview, see our [Issue Board feature page](https://about.gitlab.com/features/issueboard/)
+on about.gitlab.com or our [video introduction to Issue Boards](https://www.youtube.com/watch?v=UWsJ8tkHAa8).
+
## Use cases
+There are many ways to use GitLab Issue Boards tailored to your own preferred workflow.
+Here are some common use cases for Issue Boards.
+
+### Use cases for a single Issue Board
+
GitLab Workflow allows you to discuss proposals in issues, categorize them
with labels, and from there organize and prioritize them with Issue Boards.
@@ -65,33 +67,66 @@ beginning of the development lifecycle until deployed to production
![issue card moving](img/issue_board_move_issue_card_list.png)
-> **Notes:**
->
->- For a broader use case, please check the blog post
+### Use cases for Multiple Issue Boards
+
+With [Multiple Issue Boards](#multiple-issue-boards), available only in
+[GitLab Enterprise Edition](https://about.gitlab.com/products/),
+each team can have their own board to organize their workflow individually.
+
+#### Scrum team
+
+With multiple Issue Boards, each team has one board. Now you can move issues through each
+part of the process. For instance: **To Do**, **Doing**, and **Done**.
+
+#### Organization of topics
+
+Create lists to order things by topic and quickly change them between topics or groups,
+such as between **UX**, **Frontend**, and **Backend**. The changes will be reflected across boards,
+as changing lists will update the label accordingly.
+
+#### Advanced team handover
+
+For example, suppose we have a UX team with an Issue Board that contains:
+
+- **To Do**
+- **Doing**
+- **Frontend**
+
+When done with something, they move the card to **Frontend**. The Frontend team's board looks like:
+
+- **Frontend**
+- **Doing**
+- **Done**
+
+Cards finished by the UX team will automatically appear in the **Frontend** column when they're ready for them.
+
+NOTE: **Note:**
+For a broader use case, please see the blog post
[GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario).
->
->- For a real use case, please check why
+For a real use case example, you can read why
[Codepen decided to adopt Issue Boards](https://about.gitlab.com/2017/01/27/codepen-welcome-to-gitlab/#project-management-everything-in-one-place)
-to improve their workflow with [multiple boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards).
+to improve their workflow with multiple boards.
-## Issue Board terminology
+#### Quick assignments
-Below is a table of the definitions used for GitLab's Issue Board.
+Create lists for each of your team members and quickly drag-and-drop issues onto each team member.
-| What we call it | What it means |
-| -------------- | ------------- |
-| **Issue Board** | It represents a different view for your issues. It can have multiple lists with each list consisting of issues represented by cards. |
-| **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. |
-| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. You can re-order cards within a list. |
+## Permissions
-There are two types of lists, the ones you create based on your labels, and
-two defaults:
+[Developers and up](../permissions.md) can use all the functionality of the
+Issue Board, that is, create or delete lists and drag issues from one list to another.
-- Label list: a list based on a label. It shows all opened issues with that label.
-- **Backlog** (default): shows all open issues that does not belong to one of lists. Always appears on the very left.
-- **Closed** (default): shows all closed issues. Always appears on the very right.
+## Issue Board terminology
+
+- **Issue Board** - Each board represents a unique view for your issues. It can have multiple lists with each list consisting of issues represented by cards.
+- **List** - A column on the issue board that displays issues matching certain attributes. In addition to the default lists of 'Backlog' and 'Closed' issue, each additional list will show issues matching your chosen label or assignee.
+ - **Label list**: a list based on a label. It shows all opened issues with that label.
+ - **Assignee list**: a list which includes all issues assigned to a user.
+ - **Backlog** (default): shows all open issues that do not belong to one of the other lists. Always appears as the leftmost list.
+ - **Closed** (default): shows all closed issues. Always appears as the rightmost list.
+- **Card** - A box in the list that represents an individual issue. The information you can see on a card consists of the issue number, the issue title, the assignee, and the labels associated with the issue. You can drag cards from one list to another to change their label or assignee from that of the source list to that of the destination list.
-In short, here's a list of actions you can take in an Issue Board:
+## Actions you can take on an Issue Board
- [Create a new list](#creating-a-new-list).
- [Delete an existing list](#deleting-a-list).
@@ -129,7 +164,7 @@ right corner of the Issue Board.
![Issue Board welcome message](img/issue_board_add_list.png)
-Simply choose the label to create the list from. The new list will be inserted
+Simply choose the label or user to create the list from. The new list will be inserted
at the end of the lists, before **Done**. Moving and reordering lists is as
easy as dragging them around.
@@ -174,17 +209,19 @@ to the system so that anybody who visits the same board later will see the reord
with some exceptions.
The first time a given issue appears in any board (i.e. the first time a user
-loads a board containing that issue), it will be ordered with
-respect to other issues in that list according to [Priority order][label-priority].
+loads a board containing that issue), it will be ordered with
+respect to other issues in that list according to [Priority order](labels.md#label-priority).
+
At that point, that issue will be assigned a relative order value by the system
representing its relative order with respect to the other issues in the list. Any time
you drag-and-drop reorder that issue, its relative order value will change accordingly.
+
Also, any time that issue appears in any board when it is loaded by a user,
the updated relative order value will be used for the ordering. (It's only the first
time an issue appears that it takes from the Priority order mentioned above.) This means that
if issue `A` is drag-and-drop reordered to be above issue `B` by any user in
a given board inside your GitLab instance, any time those two issues are subsequently
-loaded in any board in the same instance (could be a different project board or a different group board, for example),
+loaded in any board in the same instance (could be a different project board or a different group board, for example),
that ordering will be maintained.
## Filtering issues
@@ -205,8 +242,8 @@ something between lists by changing a label.
A typical workflow of using the Issue Board would be:
-1. You have [created][create-labels] and [prioritized][label-priority] labels
- so that you can easily categorize your issues.
+1. You have [created](labels.md#creating-labels) and [prioritized](labels.md#label-priority)
+ labels so that you can easily categorize your issues.
1. You have a bunch of issues (ideally labeled).
1. You visit the Issue Board and start [creating lists](#creating-a-new-list) to
create a workflow.
@@ -230,21 +267,98 @@ to another list the label changes and a system not is recorded.
![Issue Board system notes](img/issue_board_system_notes.png)
-## Permissions
+## Multiple Issue Boards **[STARTER]**
-[Developers and up](../permissions.md) can use all the functionality of the
-Issue Board, that is create/delete lists and drag issues around.
+> Introduced in [GitLab Enterprise Edition 8.13](https://about.gitlab.com/2016/10/22/gitlab-8-13-released/#multiple-issue-boards-ee).
-## Group Issue Board
+Multiple Issue Boards, as the name suggests, allow for more than one Issue Board
+for a given project or group. This is great for large projects with more than one team
+or in situations where a repository is used to host the code of multiple
+products.
-> Introduced in [GitLab 10.6](https://about.gitlab.com/2018/03/22/gitlab-10-6-released/#single-group-issue-board-in-core-and-free)
+Clicking on the current board name in the upper left corner will reveal a
+menu from where you can create another Issue Board and rename or delete the
+existing one.
-Group issue board is analogous to project-level issue board and it is accessible at the group
-navigation level. A group-level issue board allows you to view all issues from all projects in that group or descendant subgroups. Similarly, you can only filter by group labels for these
+NOTE: **Note:**
+The Multiple Issue Boards feature is available for
+**projects in GitLab Starter Edition** and for **groups in GitLab Premium Edition**.
+
+![Multiple Issue Boards](img/issue_boards_multiple.png)
+
+## Configurable Issue Boards **[STARTER]**
+
+> Introduced in [GitLab Starter Edition 10.2](https://about.gitlab.com/2017/11/22/gitlab-10-2-released/#issue-boards-configuration).
+
+An Issue Board can be associated with GitLab [Milestone](milestones/index.md#milestones),
+[Labels](labels.md), Assignee and Weight
+which will automatically filter the Board issues according to these fields.
+This allows you to create unique boards according to your team's need.
+
+![Create scoped board](img/issue_board_creation.png)
+
+You can define the scope of your board when creating it or by clicking on the "Edit board" button. Once a milestone, assignee or weight is assigned to an Issue Board, you will no longer be able to filter
+through these in the search bar. In order to do that, you need to remove the desired scope (e.g. milestone, assignee or weight) from the Issue Board.
+
+![Edit board configuration](img/issue_board_edit_button.png)
+
+If you don't have editing permission in a board, you're still able to see the configuration by clicking on "View scope".
+
+![Viewing board configuration](img/issue_board_view_scope.png)
+
+## Focus mode **[STARTER]**
+
+> Introduced in [GitLab Starter 9.1](https://about.gitlab.com/2017/04/22/gitlab-9-1-released/#issue-boards-focus-mode-ees-eep).
+
+Click the button at the top right to toggle focus mode on and off. In focus mode, the navigation UI is hidden, allowing you to focus on issues in the board.
+
+![Board focus mode](img/issue_board_focus_mode.gif)
+
+## Group Issue Boards **[PREMIUM]**
+
+> Introduced in [GitLab Premium 10.0](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards).
+
+Accessible at the group navigation level, a group issue board offers the same features as a project-level board,
+but it can display issues from all projects in that
+group and its descendant subgroups. Similarly, you can only filter by group labels for these
boards. When updating milestones and labels for an issue through the sidebar update mechanism, again only
group-level objects are available.
-One group issue board per group was made available in GitLab 10.6 Core after multiple group issue boards were originally introduced in [GitLab 10.0 Premium](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards).
+NOTE: **Note:**
+Multiple group issue boards were originally introduced in [GitLab 10.0 Premium](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards) and
+one group issue board per group was made available in GitLab 10.6 Core.
+
+![Group issue board](img/group_issue_board.png)
+
+## Assignee lists **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5784) in GitLab 11.0 Premium.
+
+Like a regular list that shows all issues that have the list label, you can add
+an assignee list that shows all issues assigned to the given user.
+You can have a board with both label lists and assignee lists. To add an
+assignee list:
+
+1. Click **Add list**.
+1. Select the **Assignee list** tab.
+1. Search and click on the user you want to add as an assignee.
+
+Now that the assignee list is added, you can assign or unassign issues to that user
+by [dragging issues](#dragging-issues-between-lists) to and/or from an assignee list.
+To remove an assignee list, just as with a label list, click the trash icon.
+
+![Assignee lists](img/issue_board_assignee_lists.png)
+
+## Dragging issues between lists
+
+When dragging issues between lists, different behavior occurs depending on the source list and the target list.
+
+| | To Backlog | To Closed | To label `B` list | To assignee `Bob` list |
+| --- | --- | --- | --- | --- |
+| From Backlog | - | Issue closed | `B` added | `Bob` assigned |
+| From Closed | Issue reopened | - | Issue reopened<br/>`B` added | Issue reopened<br/>`Bob` assigned |
+| From label `A` list | `A` removed | Issue closed | `A` removed<br/>`B` added | `Bob` assigned |
+| From assignee `Alice` list | `Alice` unassigned | Issue closed | `B` added | `Alice` unassigned<br/>`Bob` assigned |
## Features per tier
@@ -261,11 +375,8 @@ Different issue board features are available in different [GitLab tiers](https:/
A few things to remember:
-- The label that corresponds to a list is hidden for issues under that list.
- Moving an issue between lists removes the label from the list it came from
and adds the label from the list it goes to.
-- When moving a card to **Done**, the label of the list it came from is removed
- and the issue gets closed.
- An issue can exist in multiple lists if it has more than one label.
- Lists are populated with issues automatically if the issues are labeled.
- Clicking on the issue title inside a card will take you to that issue.
@@ -276,10 +387,5 @@ A few things to remember:
20 will appear.
[ce-5554]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5554
-[labels]: ./labels.md
[scrum]: https://en.wikipedia.org/wiki/Scrum_(software_development)
[kanban]: https://en.wikipedia.org/wiki/Kanban_(development)
-[create-labels]: ./labels.md#create-new-labels
-[label-priority]: ./labels.md#prioritize-labels
-[landing]: https://about.gitlab.com/solutions/issueboard
-[youtube]: https://www.youtube.com/watch?v=UWsJ8tkHAa8
diff --git a/doc/user/project/issues/deleting_issues.md b/doc/user/project/issues/deleting_issues.md
index d7442104c53..536a0de8974 100644
--- a/doc/user/project/issues/deleting_issues.md
+++ b/doc/user/project/issues/deleting_issues.md
@@ -8,4 +8,6 @@ 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
+>**Note:** Only [project owners](../../permissions.md) can delete issues.
+
+[ce-2982]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2982 \ No newline at end of file
diff --git a/doc/user/project/merge_requests/squash_and_merge.md b/doc/user/project/merge_requests/squash_and_merge.md
index a6efe893853..2ec423dcf70 100644
--- a/doc/user/project/merge_requests/squash_and_merge.md
+++ b/doc/user/project/merge_requests/squash_and_merge.md
@@ -1,6 +1,6 @@
# Squash and merge
-> [Introduced][ee-1024] in [GitLab Starter][ee] 8.17, and in [GitLab CE][ce] [11.0][ce-18956].
+> [Introduced][ee-1024] in [GitLab Starter][ee] 8.17, and in [GitLab Core][ce] [11.0][ce-18956].
Combine all commits of your merge request into one and retain a clean history.
@@ -75,6 +75,6 @@ squashing can itself be considered equivalent to rebasing.
[squash-edit-form]: img/squash_edit_form.png
[squash-mr-widget]: img/squash_mr_widget.png
[ff-merge]: fast_forward_merge.md#enabling-fast-forward-merges
-[ce]: https://about.gitlab.com/products/
-[ee]: https://about.gitlab.com/products/
+[ce]: https://about.gitlab.com/pricing/
+[ee]: https://about.gitlab.com/pricing/
[revert]: revert_changes.md
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index f824756c10c..8a2f230f505 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -17,7 +17,7 @@ There are various configuration options to help GitLab server administrators:
* Enabling/disabling Git LFS support
* Changing the location of LFS object storage
-* Setting up AWS S3 compatible object storage
+* Setting up object storage supported by [Fog](http://fog.io/about/provider_documentation.html)
### Configuration for Omnibus installations
@@ -44,19 +44,31 @@ In `config/gitlab.yml`:
storage_path: /mnt/storage/lfs-objects
```
-## Storing the LFS objects in an S3-compatible object storage
+## Storing LFS objects in remote object storage
> [Introduced][ee-2760] in [GitLab Premium][eep] 10.0. Brought to GitLab Core
in 10.7.
-It is possible to store LFS objects on a remote object storage which allows you
-to offload storage to an external AWS S3 compatible service, freeing up disk
-space locally. You can also host your own S3 compatible storage decoupled from
-GitLab, with with a service such as [Minio](https://www.minio.io/).
+It is possible to store LFS objects in remote object storage which allows you
+to offload local hard disk R/W operations, and free up disk space significantly.
+GitLab is tightly integrated with `Fog`, so you can refer to its [documentation](http://fog.io/about/provider_documentation.html)
+to check which storage services can be integrated with GitLab.
+You can also use external object storage in a private local network. For example,
+[Minio](https://www.minio.io/) is a standalone object storage service, is easy to setup, and works well with GitLab instances.
-Object storage currently transfers files first to GitLab, and then on the
-object storage in a second stage. This can be done either by using a rake task
-to transfer existing objects, or in a background job after each file is received.
+GitLab provides two different options for the uploading mechanism: "Direct upload" and "Background upload".
+
+**Option 1. Direct upload**
+
+1. User pushes an lfs file to the GitLab instance
+1. GitLab-workhorse uploads the file directly to the external object storage
+1. GitLab-workhorse notifies GitLab-rails that the upload process is complete
+
+**Option 2. Background upload**
+
+1. User pushes an lfs file to the GitLab instance
+1. GitLab-rails stores the file in the local file storage
+1. GitLab-rails then uploads the file to the external object storage asynchronously
The following general settings are supported.
@@ -71,16 +83,50 @@ The following general settings are supported.
The `connection` settings match those provided by [Fog](https://github.com/fog).
-| Setting | Description | Default |
+Here is a configuration example with S3.
+
+| Setting | Description | example |
|---------|-------------|---------|
-| `provider` | Always `AWS` for compatible hosts | AWS |
-| `aws_access_key_id` | AWS credentials, or compatible | |
-| `aws_secret_access_key` | AWS credentials, or compatible | |
+| `provider` | The provider name | AWS |
+| `aws_access_key_id` | AWS credentials, or compatible | `ABC123DEF456` |
+| `aws_secret_access_key` | AWS credentials, or compatible | `ABC123DEF456ABC123DEF456ABC123DEF456` |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
| `path_style` | Set to true to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Leave as false for AWS S3 | false |
+Here is a configuration example with GCS.
+
+| Setting | Description | example |
+|---------|-------------|---------|
+| `provider` | The provider name | `Google` |
+| `google_project` | GCP project name | `gcp-project-12345` |
+| `google_client_email` | The email address of the service account | `foo@gcp-project-12345.iam.gserviceaccount.com` |
+| `google_json_key_location` | The json key path | `/path/to/gcp-project-12345-abcde.json` |
+
+_NOTE: The service account must have permission to access the bucket. [See more](https://cloud.google.com/storage/docs/authentication)_
+
+### Manual uploading to an object storage
+
+There are two ways to manually do the same thing as automatic uploading (described above).
+
+**Option 1: rake task**
+
+```
+$ rake gitlab:lfs:migrate
+```
+
+**Option 2: rails console**
+
+```
+$ sudo gitlab-rails console # Login to rails console
+
+> # Upload LFS files manually
+> LfsObject.where(file_store: [nil, 1]).find_each do |lfs_object|
+> lfs_object.file.migrate!(ObjectStorage::Store::REMOTE) if lfs_object.file.file.exists?
+> end
+```
+
### S3 for Omnibus installations
On Omnibus installations, the settings are prefixed by `lfs_object_store_`:
@@ -156,6 +202,29 @@ You can see the total storage used for LFS objects on groups and projects
in the administration area, as well as through the [groups](../../api/groups.md)
and [projects APIs](../../api/projects.md).
+## Troubleshooting: `Google::Apis::TransmissionError: execution expired`
+
+If LFS integration is configred with Google Cloud Storage and background uploads (`background_upload: true` and `direct_upload: false`),
+sidekiq workers may encouter this error. This is because the uploading timed out with very large files.
+LFS files up to 6Gb can be uploaded without any extra steps, otherwise you need to use the following workaround.
+
+```shell
+$ sudo gitlab-rails console # Login to rails console
+
+> # Set up timeouts. 20 minutes is enough to upload 30GB LFS files.
+> # These settings are only in effect for the same session, i.e. they are not effective for sidekiq workers.
+> ::Google::Apis::ClientOptions.default.open_timeout_sec = 1200
+> ::Google::Apis::ClientOptions.default.read_timeout_sec = 1200
+> ::Google::Apis::ClientOptions.default.send_timeout_sec = 1200
+
+> # Upload LFS files manually. This process does not use sidekiq at all.
+> LfsObject.where(file_store: [nil, 1]).find_each do |lfs_object|
+> lfs_object.file.migrate!(ObjectStorage::Store::REMOTE) if lfs_object.file.file.exists?
+> end
+```
+
+See more information in [!19581](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19581)
+
## Known limitations
* Support for removing unreferenced LFS objects was added in 8.14 onwards.
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index edb0c6bdc30..5dc62a30128 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -111,7 +111,7 @@ by yourself (except when an issue is due). You will only receive automatic
notifications when somebody else comments or adds changes to the ones that
you've created or mentions you.
-If a merge request becomes unmergeable, its author will be notified about the cause.
+If an open merge request becomes unmergeable due to conflict, its author will be notified about the cause.
If a user has also set the merge request to automatically merge once pipeline succeeds,
then that user will also be notified.
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 762bf616268..760cd87d4cc 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -31,7 +31,7 @@ A Todo appears in your Todos dashboard when:
- you are `@mentioned` in a comment on a commit,
- a job in the CI pipeline running for your merge request failed, but this
job is not allowed to fail.
-- a merge request becomes unmergeable, and you are either:
+- an open merge request becomes unmergeable due to conflict, and you are either:
- the author, or
- have set it to automatically merge once pipeline succeeds.
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 13cfba728fa..4b223a391ae 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -45,6 +45,7 @@ module API
present(
paginate(::Kaminari.paginate_array(branches)),
with: Entities::Branch,
+ current_user: current_user,
project: user_project,
merged_branch_names: merged_branch_names
)
@@ -63,7 +64,7 @@ module API
get do
branch = find_branch!(params[:branch])
- present branch, with: Entities::Branch, project: user_project
+ present branch, with: Entities::Branch, current_user: current_user, project: user_project
end
end
@@ -101,7 +102,7 @@ module API
end
if protected_branch.valid?
- present branch, with: Entities::Branch, project: user_project
+ present branch, with: Entities::Branch, current_user: current_user, project: user_project
else
render_api_error!(protected_branch.errors.full_messages, 422)
end
@@ -121,7 +122,7 @@ module API
protected_branch = user_project.protected_branches.find_by(name: branch.name)
protected_branch&.destroy
- present branch, with: Entities::Branch, project: user_project
+ present branch, with: Entities::Branch, current_user: current_user, project: user_project
end
desc 'Create branch' do
@@ -140,6 +141,7 @@ module API
if result[:status] == :success
present result[:branch],
with: Entities::Branch,
+ current_user: current_user,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 221ba52b490..af762db517c 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -48,7 +48,7 @@ module Backup
end
def backup_project(project)
- gitaly_migrate(:repository_backup) do |is_enabled|
+ gitaly_migrate(:repository_backup, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
backup_project_gitaly(project)
else
@@ -80,7 +80,7 @@ module Backup
end
def delete_all_repositories(name, repository_storage)
- gitaly_migrate(:delete_all_repositories) do |is_enabled|
+ gitaly_migrate(:delete_all_repositories, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
Gitlab::GitalyClient::StorageService.new(name).delete_all_repositories
else
@@ -148,7 +148,7 @@ module Backup
end
def backup_custom_hooks(project)
- gitaly_migrate(:backup_custom_hooks) do |is_enabled|
+ gitaly_migrate(:backup_custom_hooks, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_backup_custom_hooks(project)
else
@@ -159,7 +159,7 @@ module Backup
def restore_custom_hooks(project)
in_path(path_to_tars(project)) do |dir|
- gitaly_migrate(:restore_custom_hooks) do |is_enabled|
+ gitaly_migrate(:restore_custom_hooks, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_restore_custom_hooks(project, dir)
else
@@ -206,6 +206,8 @@ module Backup
progress.print " * #{wiki.full_path} ... "
begin
wiki.repository.create_from_bundle(path_to_wiki_bundle)
+ restore_custom_hooks(wiki)
+
progress.puts "[DONE]".color(:green)
rescue => e
progress.puts "[Failed] restoring #{wiki.full_path} wiki".color(:red)
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index 4bc82ecb4d6..bb9f488cd87 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -56,10 +56,12 @@ module Banzai
# Pattern to match allowed image extensions
ALLOWED_IMAGE_EXTENSIONS = /.+(jpg|png|gif|svg|bmp)\z/i.freeze
+ # Do not perform linking inside these tags.
+ IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
+
def call
doc.search(".//text()").each do |node|
- # Do not perform linking inside <code> blocks
- next unless node.ancestors('code').empty?
+ next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
# A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
# before this one, it will be converted into `[[<em>TOC</em>]]`, so it
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 6786b9d07b6..afc2ca4e362 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -25,10 +25,11 @@ module Banzai
# Only push these customizations once
return if customized?(whitelist[:transformers])
- # Allow table alignment; we whitelist specific style properties in a
+ # Allow table alignment; we whitelist specific text-align values in a
# transformer below
whitelist[:attributes]['th'] = %w(style)
whitelist[:attributes]['td'] = %w(style)
+ whitelist[:css] = { properties: ['text-align'] }
# Allow span elements
whitelist[:elements].push('span')
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index 97244159985..b32660a8341 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -92,7 +92,7 @@ module Banzai
def text
return '' unless node
- @text ||= node.text
+ @text ||= EscapeUtils.escape_html(node.text)
end
private
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index 6c5d0788a0a..e7283b2f9e8 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -74,6 +74,10 @@ module Gitlab
gl_user
end
+ def bypass_two_factor?
+ false
+ end
+
protected
def should_save?
diff --git a/lib/gitlab/auth/saml/auth_hash.rb b/lib/gitlab/auth/saml/auth_hash.rb
index c345a7e3f6c..3bc5e2864df 100644
--- a/lib/gitlab/auth/saml/auth_hash.rb
+++ b/lib/gitlab/auth/saml/auth_hash.rb
@@ -6,6 +6,17 @@ module Gitlab
Array.wrap(get_raw(Gitlab::Auth::Saml::Config.groups))
end
+ def authn_context
+ response_object = auth_hash.extra[:response_object]
+ return nil if response_object.blank?
+
+ document = response_object.decrypted_document
+ document ||= response_object.document
+ return nil if document.blank?
+
+ extract_authn_context(document)
+ end
+
private
def get_raw(key)
@@ -13,6 +24,10 @@ module Gitlab
# otherwise just the first value is returned
auth_hash.extra[:raw_info].all[key]
end
+
+ def extract_authn_context(document)
+ REXML::XPath.first(document, "//saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef/text()").to_s
+ end
end
end
end
diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb
index 5fa9581f837..625dab7c6f4 100644
--- a/lib/gitlab/auth/saml/config.rb
+++ b/lib/gitlab/auth/saml/config.rb
@@ -7,6 +7,10 @@ module Gitlab
Gitlab::Auth::OAuth::Provider.config_for('saml')
end
+ def upstream_two_factor_authn_contexts
+ options.args[:upstream_two_factor_authn_contexts]
+ end
+
def groups
options[:groups_attribute]
end
diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb
index b8c84c37cd5..6c3b75f3eb0 100644
--- a/lib/gitlab/auth/saml/user.rb
+++ b/lib/gitlab/auth/saml/user.rb
@@ -34,6 +34,10 @@ module Gitlab
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
+ def bypass_two_factor?
+ saml_config.upstream_two_factor_authn_contexts&.include?(auth_hash.authn_context)
+ end
+
protected
def saml_config
diff --git a/lib/gitlab/background_migration/cleanup_concurrent_rename.rb b/lib/gitlab/background_migration/cleanup_concurrent_rename.rb
new file mode 100644
index 00000000000..d3f366f3480
--- /dev/null
+++ b/lib/gitlab/background_migration/cleanup_concurrent_rename.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Background migration for cleaning up a concurrent column rename.
+ class CleanupConcurrentRename < CleanupConcurrentSchemaChange
+ RESCHEDULE_DELAY = 10.minutes
+
+ def cleanup_concurrent_schema_change(table, old_column, new_column)
+ cleanup_concurrent_column_rename(table, old_column, new_column)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb b/lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb
new file mode 100644
index 00000000000..54f77f184d5
--- /dev/null
+++ b/lib/gitlab/background_migration/cleanup_concurrent_schema_change.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Base class for cleaning up concurrent schema changes.
+ class CleanupConcurrentSchemaChange
+ include Database::MigrationHelpers
+
+ # table - The name of the table the migration is performed for.
+ # old_column - The name of the old (to drop) column.
+ # new_column - The name of the new column.
+ def perform(table, old_column, new_column)
+ return unless column_exists?(table, new_column)
+
+ rows_to_migrate = define_model_for(table)
+ .where(new_column => nil)
+ .where
+ .not(old_column => nil)
+
+ if rows_to_migrate.any?
+ BackgroundMigrationWorker.perform_in(
+ RESCHEDULE_DELAY,
+ self.class.name,
+ [table, old_column, new_column]
+ )
+ else
+ cleanup_concurrent_schema_change(table, old_column, new_column)
+ end
+ end
+
+ # These methods are necessary so we can re-use the migration helpers in
+ # this class.
+ def connection
+ ActiveRecord::Base.connection
+ end
+
+ def method_missing(name, *args, &block)
+ connection.__send__(name, *args, &block) # rubocop: disable GitlabSecurity/PublicSend
+ end
+
+ def respond_to_missing?(*args)
+ connection.respond_to?(*args) || super
+ end
+
+ def define_model_for(table)
+ Class.new(ActiveRecord::Base) do
+ self.table_name = table
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb b/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb
index de622f657b2..48411095dbb 100644
--- a/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb
+++ b/lib/gitlab/background_migration/cleanup_concurrent_type_change.rb
@@ -2,52 +2,12 @@
module Gitlab
module BackgroundMigration
- # Background migration for cleaning up a concurrent column rename.
- class CleanupConcurrentTypeChange
- include Database::MigrationHelpers
-
+ # Background migration for cleaning up a concurrent column type changeb.
+ class CleanupConcurrentTypeChange < CleanupConcurrentSchemaChange
RESCHEDULE_DELAY = 10.minutes
- # table - The name of the table the migration is performed for.
- # old_column - The name of the old (to drop) column.
- # new_column - The name of the new column.
- def perform(table, old_column, new_column)
- return unless column_exists?(:issues, new_column)
-
- rows_to_migrate = define_model_for(table)
- .where(new_column => nil)
- .where
- .not(old_column => nil)
-
- if rows_to_migrate.any?
- BackgroundMigrationWorker.perform_in(
- RESCHEDULE_DELAY,
- 'CleanupConcurrentTypeChange',
- [table, old_column, new_column]
- )
- else
- cleanup_concurrent_column_type_change(table, old_column)
- end
- end
-
- # These methods are necessary so we can re-use the migration helpers in
- # this class.
- def connection
- ActiveRecord::Base.connection
- end
-
- def method_missing(name, *args, &block)
- connection.__send__(name, *args, &block) # rubocop: disable GitlabSecurity/PublicSend
- end
-
- def respond_to_missing?(*args)
- connection.respond_to?(*args) || super
- end
-
- def define_model_for(table)
- Class.new(ActiveRecord::Base) do
- self.table_name = table
- end
+ def cleanup_concurrent_schema_change(table, old_column, new_column)
+ cleanup_concurrent_column_type_change(table, old_column)
end
end
end
diff --git a/lib/gitlab/cache/request_cache.rb b/lib/gitlab/cache/request_cache.rb
index ecc85f847d4..671b8e7e1b1 100644
--- a/lib/gitlab/cache/request_cache.rb
+++ b/lib/gitlab/cache/request_cache.rb
@@ -1,41 +1,6 @@
module Gitlab
module Cache
- # This module provides a simple way to cache values in RequestStore,
- # and the cache key would be based on the class name, method name,
- # optionally customized instance level values, optionally customized
- # method level values, and optional method arguments.
- #
- # A simple example:
- #
- # class UserAccess
- # extend Gitlab::Cache::RequestCache
- #
- # request_cache_key do
- # [user&.id, project&.id]
- # end
- #
- # request_cache def can_push_to_branch?(ref)
- # # ...
- # end
- # end
- #
- # This way, the result of `can_push_to_branch?` would be cached in
- # `RequestStore.store` based on the cache key. If RequestStore is not
- # currently active, then it would be stored in a hash saved in an
- # instance variable, so the cache logic would be the same.
- # Here's another example using customized method level values:
- #
- # class Commit
- # extend Gitlab::Cache::RequestCache
- #
- # def author
- # User.find_by_any_email(author_email.downcase)
- # end
- # request_cache(:author) { author_email.downcase }
- # end
- #
- # So that we could have different strategies for different methods
- #
+ # See https://docs.gitlab.com/ee/development/utilities.html#requestcache
module RequestCache
def self.extended(klass)
return if klass < self
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index d00e5b07f95..222aa06b800 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -4,6 +4,9 @@ module Gitlab
class Collection
class Item
def initialize(key:, value:, public: true, file: false)
+ raise ArgumentError, "`value` must be of type String, while it was: #{value.class}" unless
+ value.is_a?(String) || value.nil?
+
@variable = {
key: key, value: value, public: public, file: file
}
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index 3cac007a42c..f64e3d53138 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -33,7 +33,13 @@ module Gitlab
end
def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
- query = arel_table
+ arel_from = if Gitlab.rails5?
+ arel_table.from
+ else
+ arel_table
+ end
+
+ query = arel_from
.from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name))
.project(average([arel_table[column_sym]], 'median'))
.where(
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index c21bae5e16b..4fe5b4cc835 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -596,6 +596,97 @@ module Gitlab
end
end
+ # Renames a column using a background migration.
+ #
+ # Because this method uses a background migration it's more suitable for
+ # large tables. For small tables it's better to use
+ # `rename_column_concurrently` since it can complete its work in a much
+ # shorter amount of time and doesn't rely on Sidekiq.
+ #
+ # Example usage:
+ #
+ # rename_column_using_background_migration(
+ # :users,
+ # :feed_token,
+ # :rss_token
+ # )
+ #
+ # table - The name of the database table containing the column.
+ #
+ # old - The old column name.
+ #
+ # new - The new column name.
+ #
+ # type - The type of the new column. If no type is given the old column's
+ # type is used.
+ #
+ # batch_size - The number of rows to schedule in a single background
+ # migration.
+ #
+ # interval - The time interval between every background migration.
+ def rename_column_using_background_migration(
+ table,
+ old_column,
+ new_column,
+ type: nil,
+ batch_size: 10_000,
+ interval: 10.minutes
+ )
+
+ check_trigger_permissions!(table)
+
+ old_col = column_for(table, old_column)
+ new_type = type || old_col.type
+ max_index = 0
+
+ add_column(table, new_column, new_type,
+ limit: old_col.limit,
+ precision: old_col.precision,
+ scale: old_col.scale)
+
+ # We set the default value _after_ adding the column so we don't end up
+ # updating any existing data with the default value. This isn't
+ # necessary since we copy over old values further down.
+ change_column_default(table, new_column, old_col.default) if old_col.default
+
+ install_rename_triggers(table, old_column, new_column)
+
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = table
+
+ include ::EachBatch
+ end
+
+ # Schedule the jobs that will copy the data from the old column to the
+ # new one. Rows with NULL values in our source column are skipped since
+ # the target column is already NULL at this point.
+ model.where.not(old_column => nil).each_batch(of: batch_size) do |batch, index|
+ start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
+ max_index = index
+
+ BackgroundMigrationWorker.perform_in(
+ index * interval,
+ 'CopyColumn',
+ [table, old_column, new_column, start_id, end_id]
+ )
+ end
+
+ # Schedule the renaming of the column to happen (initially) 1 hour after
+ # the last batch finished.
+ BackgroundMigrationWorker.perform_in(
+ (max_index * interval) + 1.hour,
+ 'CleanupConcurrentRename',
+ [table, old_column, new_column]
+ )
+
+ if perform_background_migration_inline?
+ # To ensure the schema is up to date immediately we perform the
+ # migration inline in dev / test environments.
+ Gitlab::BackgroundMigration.steal('CopyColumn')
+ Gitlab::BackgroundMigration.steal('CleanupConcurrentRename')
+ end
+ end
+
def perform_background_migration_inline?
Rails.env.test? || Rails.env.development?
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 2820293ad5c..40bcfa20e7d 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -130,11 +130,13 @@ module Gitlab
# Array of Gitlab::Diff::Line objects
def diff_lines
- @diff_lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a
+ @diff_lines ||=
+ Gitlab::Diff::Parser.new.parse(raw_diff.each_line, diff_file: self).to_a
end
def highlighted_diff_lines
- @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
+ @highlighted_diff_lines ||=
+ Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
end
# Array[<Hash>] with right/left keys that contains Gitlab::Diff::Line objects which text is hightlighted
@@ -239,8 +241,33 @@ module Gitlab
simple_viewer.is_a?(DiffViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?)
end
+ # This adds the bottom match line to the array if needed. It contains
+ # the data to load more context lines.
+ def diff_lines_for_serializer
+ lines = highlighted_diff_lines
+
+ return if lines.empty?
+
+ last_line = lines.last
+
+ if last_line.new_pos < total_blob_lines(blob)
+ match_line = Gitlab::Diff::Line.new("", 'match', nil, last_line.old_pos, last_line.new_pos)
+ lines.push(match_line)
+ end
+
+ lines
+ end
+
private
+ def total_blob_lines(blob)
+ @total_lines ||= begin
+ line_count = blob.lines.size
+ line_count -= 1 if line_count > 0 && blob.lines.last.blank?
+ line_count
+ end
+ end
+
# We can't use Object#try because Blob doesn't inherit from Object, but
# from BasicObject (via SimpleDelegator).
def try_blobs(meth)
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index a1e904cfef4..2b3ebfbb9ff 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -1,22 +1,26 @@
module Gitlab
module Diff
class Line
- attr_reader :type, :index, :old_pos, :new_pos
+ attr_reader :line_code, :type, :index, :old_pos, :new_pos
attr_writer :rich_text
attr_accessor :text
- def initialize(text, type, index, old_pos, new_pos, parent_file: nil)
+ def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil)
@text, @type, @index = text, type, index
@old_pos, @new_pos = old_pos, new_pos
@parent_file = parent_file
+
+ # When line code is not provided from cache store we build it
+ # using the parent_file(Diff::File or Conflict::File).
+ @line_code = line_code || calculate_line_code
end
def self.init_from_hash(hash)
- new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos])
+ new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos], line_code: hash[:line_code])
end
def serialize_keys
- @serialize_keys ||= %i(text type index old_pos new_pos)
+ @serialize_keys ||= %i(line_code text type index old_pos new_pos)
end
def to_hash
@@ -62,20 +66,37 @@ module Gitlab
end
def rich_text
- @parent_file.highlight_lines! if @parent_file && !@rich_text
+ @parent_file.try(:highlight_lines!) if @parent_file && !@rich_text
@rich_text
end
+ def meta_positions
+ return unless meta?
+
+ {
+ old_pos: old_pos,
+ new_pos: new_pos
+ }
+ end
+
def as_json(opts = nil)
{
+ line_code: line_code,
type: type,
old_line: old_line,
new_line: new_line,
text: text,
- rich_text: rich_text || text
+ rich_text: rich_text || text,
+ meta_data: meta_positions
}
end
+
+ private
+
+ def calculate_line_code
+ @parent_file&.line_code(self)
+ end
end
end
end
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 8302f30a0a2..7ae7ed286ed 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -3,7 +3,7 @@ module Gitlab
class Parser
include Enumerable
- def parse(lines)
+ def parse(lines, diff_file: nil)
return [] if lines.blank?
@lines = lines
@@ -31,17 +31,17 @@ module Gitlab
next if line_old <= 1 && line_new <= 1 # top of file
- yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: diff_file)
line_obj_index += 1
next
elsif line[0] == '\\'
type = "#{context}-nonewline"
- yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: diff_file)
line_obj_index += 1
else
type = identification_type(line)
- yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: diff_file)
line_obj_index += 1
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 156d077a69c..604bb11e712 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -21,13 +21,31 @@ module Gitlab
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
class << self
- def find(repository, sha, path)
- Gitlab::GitalyClient.migrate(:project_raw_show) do |is_enabled|
- if is_enabled
- find_by_gitaly(repository, sha, path)
- else
- find_by_rugged(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
- end
+ def find(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
+ return unless path
+
+ path = path.sub(%r{\A/*}, '')
+ path = '/' if path.empty?
+ name = File.basename(path)
+
+ # Gitaly will think that setting the limit to 0 means unlimited, while
+ # the client might only need the metadata and thus set the limit to 0.
+ # In this method we'll then set the limit to 1, but clear the byte of data
+ # that we got back so for the outside world it looks like the limit was
+ # actually 0.
+ req_limit = limit == 0 ? 1 : limit
+
+ entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, req_limit)
+ return unless entry
+
+ entry.data = "" if limit == 0
+
+ case entry.type
+ when :COMMIT
+ new(id: entry.oid, name: name, size: 0, data: '', path: path, commit_id: sha)
+ when :BLOB
+ new(id: entry.oid, name: name, size: entry.size, data: entry.data.dup, mode: entry.mode.to_s(8),
+ path: path, commit_id: sha, binary: binary?(entry.data))
end
end
@@ -56,7 +74,7 @@ module Gitlab
repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a
else
blob_references.map do |sha, path|
- find_by_rugged(repository, sha, path, limit: blob_size_limit)
+ find(repository, sha, path, limit: blob_size_limit)
end
end
end
@@ -136,85 +154,6 @@ module Gitlab
)
end
- def find_by_gitaly(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
- return unless path
-
- path = path.sub(%r{\A/*}, '')
- path = '/' if path.empty?
- name = File.basename(path)
-
- # Gitaly will think that setting the limit to 0 means unlimited, while
- # the client might only need the metadata and thus set the limit to 0.
- # In this method we'll then set the limit to 1, but clear the byte of data
- # that we got back so for the outside world it looks like the limit was
- # actually 0.
- req_limit = limit == 0 ? 1 : limit
-
- entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, req_limit)
- return unless entry
-
- entry.data = "" if limit == 0
-
- case entry.type
- when :COMMIT
- new(
- id: entry.oid,
- name: name,
- size: 0,
- data: '',
- path: path,
- commit_id: sha
- )
- when :BLOB
- new(
- id: entry.oid,
- name: name,
- size: entry.size,
- data: entry.data.dup,
- mode: entry.mode.to_s(8),
- path: path,
- commit_id: sha,
- binary: binary?(entry.data)
- )
- end
- end
-
- def find_by_rugged(repository, sha, path, limit:)
- return unless path
-
- # Strip any leading / characters from the path
- path = path.sub(%r{\A/*}, '')
-
- rugged_commit = repository.lookup(sha)
- root_tree = rugged_commit.tree
-
- blob_entry = find_entry_by_path(repository, root_tree.oid, *path.split('/'))
-
- return nil unless blob_entry
-
- if blob_entry[:type] == :commit
- submodule_blob(blob_entry, path, sha)
- else
- blob = repository.lookup(blob_entry[:oid])
-
- if blob
- new(
- id: blob.oid,
- name: blob_entry[:name],
- size: blob.size,
- # Rugged::Blob#content is expensive; don't call it if we don't have to.
- data: limit.zero? ? '' : blob.content(limit),
- mode: blob_entry[:filemode].to_s(8),
- path: path,
- commit_id: sha,
- binary: blob.binary?
- )
- end
- end
- rescue Rugged::ReferenceError
- nil
- end
-
def rugged_raw(repository, sha, limit:)
blob = repository.lookup(sha)
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index c9806cdb85f..341768752dc 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -381,15 +381,11 @@ module Gitlab
# empty repo. See Repository#diff for keys allowed in the +options+
# hash.
def diff_from_parent(options = {})
- Gitlab::GitalyClient.migrate(:commit_raw_diffs) do |is_enabled|
- if is_enabled
- @repository.gitaly_commit_client.diff_from_parent(self, options)
- else
- rugged_diff_from_parent(options)
- end
- end
+ @repository.gitaly_commit_client.diff_from_parent(self, options)
end
+ # Not to be called directly, but right now its used for tests and in old
+ # migrations
def rugged_diff_from_parent(options = {})
options ||= {}
break_rewrites = options[:break_rewrites]
diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb
index 8475645971e..5ff15a787f0 100644
--- a/lib/gitlab/git/gitlab_projects.rb
+++ b/lib/gitlab/git/gitlab_projects.rb
@@ -61,22 +61,15 @@ module Gitlab
end
def fetch_remote(name, timeout, force:, tags:, ssh_key: nil, known_hosts: nil, prune: true)
- tags_option = tags ? '--tags' : '--no-tags'
-
logger.info "Fetching remote #{name} for repository #{repository_absolute_path}."
- cmd = %W(#{Gitlab.config.git.bin_path} fetch #{name} --quiet)
- cmd << '--prune' if prune
- cmd << '--force' if force
- cmd << tags_option
+ cmd = fetch_remote_command(name, tags, prune, force)
setup_ssh_auth(ssh_key, known_hosts) do |env|
- success = run_with_timeout(cmd, timeout, repository_absolute_path, env)
-
- unless success
- logger.error "Fetching remote #{name} for repository #{repository_absolute_path} failed."
+ run_with_timeout(cmd, timeout, repository_absolute_path, env).tap do |success|
+ unless success
+ logger.error "Fetching remote #{name} for repository #{repository_absolute_path} failed."
+ end
end
-
- success
end
end
@@ -202,6 +195,14 @@ module Gitlab
private
+ def fetch_remote_command(name, tags, prune, force)
+ %W(#{Gitlab.config.git.bin_path} fetch #{name} --quiet).tap do |cmd|
+ cmd << '--prune' if prune
+ cmd << '--force' if force
+ cmd << (tags ? '--tags' : '--no-tags')
+ end
+ end
+
def git_import_repository(source, timeout)
# Skip import if repo already exists
return false if File.exist?(repository_absolute_path)
diff --git a/lib/gitlab/git/remote_mirror.rb b/lib/gitlab/git/remote_mirror.rb
index ebe46722890..e4743b4db0a 100644
--- a/lib/gitlab/git/remote_mirror.rb
+++ b/lib/gitlab/git/remote_mirror.rb
@@ -7,81 +7,8 @@ module Gitlab
end
def update(only_branches_matching: [])
- @repository.gitaly_migrate(:remote_update_remote_mirror) do |is_enabled|
- if is_enabled
- gitaly_update(only_branches_matching)
- else
- rugged_update(only_branches_matching)
- end
- end
- end
-
- private
-
- def gitaly_update(only_branches_matching)
- @repository.gitaly_remote_client.update_remote_mirror(@ref_name, only_branches_matching)
- end
-
- def rugged_update(only_branches_matching)
- local_branches = refs_obj(@repository.local_branches, only_refs_matching: only_branches_matching)
- remote_branches = refs_obj(@repository.remote_branches(@ref_name), only_refs_matching: only_branches_matching)
-
- updated_branches = changed_refs(local_branches, remote_branches)
- push_branches(updated_branches.keys) if updated_branches.present?
-
- delete_refs(local_branches, remote_branches)
-
- local_tags = refs_obj(@repository.tags)
- remote_tags = refs_obj(@repository.remote_tags(@ref_name))
-
- updated_tags = changed_refs(local_tags, remote_tags)
- @repository.push_remote_branches(@ref_name, updated_tags.keys) if updated_tags.present?
-
- delete_refs(local_tags, remote_tags)
- end
-
- def refs_obj(refs, only_refs_matching: [])
- refs.each_with_object({}) do |ref, refs|
- next if only_refs_matching.present? && !only_refs_matching.include?(ref.name)
-
- refs[ref.name] = ref
- end
- end
-
- def changed_refs(local_refs, remote_refs)
- local_refs.select do |ref_name, ref|
- remote_ref = remote_refs[ref_name]
-
- remote_ref.nil? || ref.dereferenced_target != remote_ref.dereferenced_target
- end
- end
-
- def push_branches(branches)
- default_branch, branches = branches.partition do |branch|
- @repository.root_ref == branch
- end
-
- # Push the default branch first so it works fine when remote mirror is empty.
- branches.unshift(*default_branch)
-
- @repository.push_remote_branches(@ref_name, branches)
- end
-
- def delete_refs(local_refs, remote_refs)
- refs = refs_to_delete(local_refs, remote_refs)
-
- @repository.delete_remote_branches(@ref_name, refs.keys) if refs.present?
- end
-
- def refs_to_delete(local_refs, remote_refs)
- default_branch_id = @repository.commit.id
-
- remote_refs.select do |remote_ref_name, remote_ref|
- next false if local_refs[remote_ref_name] # skip if branch or tag exist in local repo
-
- remote_ref_id = remote_ref.dereferenced_target.try(:id)
-
- remote_ref_id && @repository.rugged_is_ancestor?(remote_ref_id, default_branch_id)
+ @repository.wrapped_gitaly_errors do
+ @repository.gitaly_remote_client.update_remote_mirror(@ref_name, only_branches_matching)
end
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 77543206e13..88944cd62ea 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -403,13 +403,7 @@ module Gitlab
# Return repo size in megabytes
def size
- size = gitaly_migrate(:repository_size) do |is_enabled|
- if is_enabled
- size_by_gitaly
- else
- size_by_shelling_out
- end
- end
+ size = gitaly_repository_client.repository_size
(size.to_f / 1024).round(2)
end
@@ -529,32 +523,17 @@ module Gitlab
def raw_changes_between(old_rev, new_rev)
@raw_changes_between ||= {}
- @raw_changes_between[[old_rev, new_rev]] ||= begin
- return [] if new_rev.blank? || new_rev == Gitlab::Git::BLANK_SHA
+ @raw_changes_between[[old_rev, new_rev]] ||=
+ begin
+ return [] if new_rev.blank? || new_rev == Gitlab::Git::BLANK_SHA
- gitaly_migrate(:raw_changes_between) do |is_enabled|
- if is_enabled
+ wrapped_gitaly_errors do
gitaly_repository_client.raw_changes_between(old_rev, new_rev)
.each_with_object([]) do |msg, arr|
msg.raw_changes.each { |change| arr << ::Gitlab::Git::RawDiffChange.new(change) }
end
- else
- result = []
-
- circuit_breaker.perform do
- Open3.pipeline_r(git_diff_cmd(old_rev, new_rev), format_git_cat_file_script, git_cat_file_cmd) do |last_stdout, wait_threads|
- last_stdout.each_line { |line| result << ::Gitlab::Git::RawDiffChange.new(line.chomp!) }
-
- if wait_threads.any? { |waiter| !waiter.value&.success? }
- raise ::Gitlab::Git::Repository::GitError, "Unabled to obtain changes between #{old_rev} and #{new_rev}"
- end
- end
- end
-
- result
end
end
- end
rescue ArgumentError => e
raise Gitlab::Git::Repository::GitError.new(e)
end
@@ -570,24 +549,9 @@ module Gitlab
end
end
- # Gitaly note: JV: check gitlab-ee before removing this method.
- def rugged_is_ancestor?(ancestor_id, descendant_id)
- return false if ancestor_id.nil? || descendant_id.nil?
-
- rugged_merge_base(ancestor_id, descendant_id) == ancestor_id
- rescue Rugged::OdbError
- false
- end
-
# Returns true is +from+ is direct ancestor to +to+, otherwise false
def ancestor?(from, to)
- Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
- if is_enabled
- gitaly_commit_client.ancestor?(from, to)
- else
- rugged_is_ancestor?(from, to)
- end
- end
+ gitaly_commit_client.ancestor?(from, to)
end
def merged_branch_names(branch_names = [])
@@ -628,17 +592,7 @@ module Gitlab
def ref_name_for_sha(ref_path, sha)
raise ArgumentError, "sha can't be empty" unless sha.present?
- gitaly_migrate(:find_ref_name) do |is_enabled|
- if is_enabled
- gitaly_ref_client.find_ref_name(sha, ref_path)
- else
- args = %W(for-each-ref --count=1 #{ref_path} --contains #{sha})
-
- # Not found -> ["", 0]
- # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
- run_git(args).first.split.last
- end
- end
+ gitaly_ref_client.find_ref_name(sha, ref_path)
end
# Get refs hash which key is is the commit id
@@ -730,6 +684,10 @@ module Gitlab
end
end
+ def update_branch(branch_name, user:, newrev:, oldrev:)
+ OperationService.new(user, self).update_branch(branch_name, newrev, oldrev)
+ end
+
def rm_branch(branch_name, user:)
gitaly_migrate(:operation_user_delete_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
@@ -961,13 +919,7 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/327
def ls_files(ref)
- gitaly_migrate(:ls_files) do |is_enabled|
- if is_enabled
- gitaly_ls_files(ref)
- else
- git_ls_files(ref)
- end
- end
+ gitaly_commit_client.ls_files(ref)
end
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/328
@@ -1015,29 +967,8 @@ module Gitlab
end
def languages(ref = nil)
- gitaly_migrate(:commit_languages, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_commit_client.languages(ref)
- else
- ref ||= rugged.head.target_id
- languages = Linguist::Repository.new(rugged, ref).languages
- total = languages.map(&:last).sum
-
- languages = languages.map do |language|
- name, share = language
- color = Linguist::Language[name].color || "##{Digest::SHA256.hexdigest(name)[0...6]}"
- {
- value: (share.to_f * 100 / total).round(2),
- label: name,
- color: color,
- highlight: color
- }
- end
-
- languages.sort do |x, y|
- y[:value] <=> x[:value]
- end
- end
+ wrapped_gitaly_errors do
+ gitaly_commit_client.languages(ref)
end
end
@@ -1195,16 +1126,7 @@ module Gitlab
end
def create_from_bundle(bundle_path)
- gitaly_migrate(:create_repo_from_bundle) do |is_enabled|
- if is_enabled
- gitaly_repository_client.create_from_bundle(bundle_path)
- else
- run_git!(%W(clone --bare -- #{bundle_path} #{path}), chdir: nil)
- self.class.create_hooks(path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path))
- end
- end
-
- true
+ gitaly_repository_client.create_from_bundle(bundle_path)
end
def create_from_snapshot(url, auth)
@@ -1305,16 +1227,10 @@ module Gitlab
return unless full_path.present?
# This guard avoids Gitaly log/error spam
- unless exists?
- raise NoRepository, 'repository does not exist'
- end
+ raise NoRepository, 'repository does not exist' unless exists?
- gitaly_migrate(:write_config) do |is_enabled|
- if is_enabled
- gitaly_repository_client.write_config(full_path: full_path)
- else
- rugged_write_config(full_path: full_path)
- end
+ wrapped_gitaly_errors do
+ gitaly_repository_client.write_config(full_path: full_path)
end
end
@@ -1413,8 +1329,11 @@ module Gitlab
end
def can_be_merged?(source_sha, target_branch)
- target_sha = find_branch(target_branch, true).target
- !gitaly_conflicts_client(source_sha, target_sha).conflicts?
+ if target_sha = find_branch(target_branch, true)&.target
+ !gitaly_conflicts_client(source_sha, target_sha).conflicts?
+ else
+ false
+ end
end
def search_files_by_name(query, ref)
@@ -1840,41 +1759,6 @@ module Gitlab
commit(sha)
end
- def size_by_shelling_out
- popen(%w(du -sk), path).first.strip.to_i
- end
-
- def size_by_gitaly
- gitaly_repository_client.repository_size
- end
-
- def gitaly_ls_files(ref)
- gitaly_commit_client.ls_files(ref)
- end
-
- def git_ls_files(ref)
- actual_ref = ref || root_ref
-
- begin
- sha_from_ref(actual_ref)
- rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
- # Return an empty array if the ref wasn't found
- return []
- end
-
- cmd = %W(ls-tree -r --full-tree --full-name -- #{actual_ref})
- raw_output, _status = run_git(cmd)
-
- lines = raw_output.split("\n").map do |f|
- stuff, path = f.split("\t")
- _mode, type, _sha = stuff.split(" ")
- path if type == "blob"
- # Contain only blob type
- end
-
- lines.compact
- end
-
# Returns true if the given ref name exists
#
# Ref names must start with `refs/`.
@@ -2073,8 +1957,7 @@ module Gitlab
rebase_sha = run_git!(%w(rev-parse HEAD), chdir: rebase_path, env: env).strip
- Gitlab::Git::OperationService.new(user, self)
- .update_branch(branch, rebase_sha, branch_sha)
+ update_branch(branch, user: user, newrev: rebase_sha, oldrev: branch_sha)
rebase_sha
end
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index a39b3bc158c..7b2b3bedf04 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -22,27 +22,27 @@ module Gitlab
def init_metrics
metrics = {}
- metrics[:sampler_duration] = Metrics.histogram(with_prefix(:sampler_duration, :seconds), 'Sampler time', { worker: nil })
- metrics[:total_time] = Metrics.gauge(with_prefix(:gc, :time_total), 'Total GC time', labels, :livesum)
+ metrics[:sampler_duration] = Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels)
+ metrics[:total_time] = Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels)
GC.stat.keys.each do |key|
- metrics[key] = Metrics.gauge(with_prefix(:gc, key), to_doc_string(key), labels, :livesum)
+ metrics[key] = Metrics.gauge(with_prefix(:gc_stat, key), to_doc_string(key), labels, :livesum)
end
- metrics[:objects_total] = Metrics.gauge(with_prefix(:objects, :total), 'Objects total', labels.merge(class: nil), :livesum)
- metrics[:memory_usage] = Metrics.gauge(with_prefix(:memory, :usage_total), 'Memory used total', labels, :livesum)
- metrics[:file_descriptors] = Metrics.gauge(with_prefix(:file, :descriptors_total), 'File descriptors total', labels, :livesum)
+ metrics[:memory_usage] = Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum)
+ metrics[:file_descriptors] = Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum)
metrics
end
def sample
start_time = System.monotonic_time
- sample_gc
- metrics[:memory_usage].set(labels, System.memory_usage)
- metrics[:file_descriptors].set(labels, System.file_descriptor_count)
+ metrics[:memory_usage].set(labels.merge(worker_label), System.memory_usage)
+ metrics[:file_descriptors].set(labels.merge(worker_label), System.file_descriptor_count)
+
+ sample_gc
- metrics[:sampler_duration].observe(labels.merge(worker_label), System.monotonic_time - start_time)
+ metrics[:sampler_duration].increment(labels, System.monotonic_time - start_time)
ensure
GC::Profiler.clear
end
@@ -50,11 +50,13 @@ module Gitlab
private
def sample_gc
- metrics[:total_time].set(labels, GC::Profiler.total_time * 1000)
-
+ # Collect generic GC stats.
GC.stat.each do |key, value|
metrics[key].set(labels, value)
end
+
+ # Collect the GC time since last sample in float seconds.
+ metrics[:total_time].increment(labels, GC::Profiler.total_time)
end
def worker_label
diff --git a/lib/gitlab/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb
index ccfe0d6bed3..a502ad8a541 100644
--- a/lib/gitlab/request_forgery_protection.rb
+++ b/lib/gitlab/request_forgery_protection.rb
@@ -5,7 +5,7 @@
module Gitlab
module RequestForgeryProtection
class Controller < ActionController::Base
- protect_from_forgery with: :exception
+ protect_from_forgery with: :exception, prepend: true
rescue_from ActionController::InvalidAuthenticityToken do |e|
logger.warn "This CSRF token verification failure is handled internally by `GitLab::RequestForgeryProtection`"
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 59a222b086c..dff0c97eeb4 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -24,7 +24,6 @@ module Gitlab
installation_type: Gitlab::INSTALLATION_TYPE,
active_user_count: User.active.count,
recorded_at: Time.now,
- mattermost_enabled: Gitlab.config.mattermost.enabled,
edition: 'CE'
}
@@ -91,13 +90,14 @@ module Gitlab
def features_usage_data_ce
{
- signup: Gitlab::CurrentSettings.allow_signup?,
- ldap: Gitlab.config.ldap.enabled,
- gravatar: Gitlab::CurrentSettings.gravatar_enabled?,
- omniauth: Gitlab.config.omniauth.enabled,
- reply_by_email: Gitlab::IncomingEmail.enabled?,
- container_registry: Gitlab.config.registry.enabled,
- gitlab_shared_runners: Gitlab.config.gitlab_ci.shared_runners_enabled
+ container_registry_enabled: Gitlab.config.registry.enabled,
+ gitlab_shared_runners_enabled: Gitlab.config.gitlab_ci.shared_runners_enabled,
+ gravatar_enabled: Gitlab::CurrentSettings.gravatar_enabled?,
+ ldap_enabled: Gitlab.config.ldap.enabled,
+ mattermost_enabled: Gitlab.config.mattermost.enabled,
+ omniauth_enabled: Gitlab.config.omniauth.enabled,
+ reply_by_email_enabled: Gitlab::IncomingEmail.enabled?,
+ signup_enabled: Gitlab::CurrentSettings.allow_signup?
}
end
diff --git a/lib/gitlab/verify/uploads.rb b/lib/gitlab/verify/uploads.rb
index 01f09ab8df7..73fc43cb590 100644
--- a/lib/gitlab/verify/uploads.rb
+++ b/lib/gitlab/verify/uploads.rb
@@ -12,7 +12,7 @@ module Gitlab
private
def all_relation
- Upload.all
+ Upload.all.preload(:model)
end
def local?(upload)
diff --git a/lib/mysql_zero_date.rb b/lib/mysql_zero_date.rb
new file mode 100644
index 00000000000..64634f789da
--- /dev/null
+++ b/lib/mysql_zero_date.rb
@@ -0,0 +1,18 @@
+# Disable NO_ZERO_DATE mode for mysql in rails 5.
+# We use zero date as a default value
+# (config/initializers/active_record_mysql_timestamp.rb), in
+# Rails 5 using zero date fails by default (https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/75450216)
+# and NO_ZERO_DATE has to be explicitly disabled. Disabling strict mode
+# is not sufficient.
+
+require 'active_record/connection_adapters/abstract_mysql_adapter'
+
+module MysqlZeroDate
+ def configure_connection
+ super
+
+ @connection.query "SET @@SESSION.sql_mode = REPLACE(@@SESSION.sql_mode, 'NO_ZERO_DATE', '');" # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+end
+
+ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.prepend(MysqlZeroDate) if Gitlab.rails5?
diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake
index 4b4881cecb8..4bec013a141 100644
--- a/lib/tasks/flay.rake
+++ b/lib/tasks/flay.rake
@@ -1,6 +1,6 @@
desc 'Code duplication analyze via flay'
task :flay do
- output = `bundle exec flay --mass 35 app/ lib/gitlab/ 2> #{File::NULL}`
+ output = `bundle exec flay --mass 35 app/ lib/gitlab/ ee/ 2> #{File::NULL}`
if output.include?("Similar code found") || output.include?("IDENTICAL code found")
puts output
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index d7c40cb6a12..21998dd2f5b 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -4,7 +4,7 @@ namespace :gettext do
# Customize list of translatable files
# See: https://github.com/grosser/gettext_i18n_rails#customizing-list-of-translatable-files
def files_to_translate
- folders = %W(app lib config #{locale_path}).join(',')
+ folders = %W(ee app lib config #{locale_path}).join(',')
exts = %w(rb erb haml slim rhtml js jsx vue handlebars hbs mustache).join(',')
Dir.glob(
@@ -16,7 +16,6 @@ namespace :gettext do
# See: https://gitlab.com/gitlab-org/gitlab-ce/issues/33014#note_31218998
FileUtils.touch(File.join(Rails.root, 'locale/gitlab.pot'))
- Rake::Task['gettext:pack'].invoke
Rake::Task['gettext:po_to_json'].invoke
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index db5c183bac3..926bd708532 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: 2018-06-13 14:05+0200\n"
-"PO-Revision-Date: 2018-06-13 14:05+0200\n"
+"POT-Creation-Date: 2018-06-20 16:52+0300\n"
+"PO-Revision-Date: 2018-06-20 16:52+0300\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -18,6 +18,11 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+msgid "%d changed file"
+msgid_plural "%d changed files"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] ""
@@ -79,6 +84,9 @@ msgid_plural "%{count} participants"
msgstr[0] ""
msgstr[1] ""
+msgid "%{filePath} deleted"
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
@@ -828,9 +836,6 @@ msgstr ""
msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
msgstr ""
-msgid "Can run untagged jobs"
-msgstr ""
-
msgid "Cancel"
msgstr ""
@@ -996,6 +1001,9 @@ msgstr ""
msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
+msgid "Click to expand it."
+msgstr ""
+
msgid "Click to expand text"
msgstr ""
@@ -1176,7 +1184,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
-msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -1269,9 +1283,6 @@ msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|See zones"
-msgstr ""
-
msgid "ClusterIntegration|Select machine type"
msgstr ""
@@ -1362,10 +1373,10 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
-msgid "Comment and resolve discussion"
+msgid "Comment & resolve discussion"
msgstr ""
-msgid "Comment and unresolve discussion"
+msgid "Comment & unresolve discussion"
msgstr ""
msgid "Comments"
@@ -1582,6 +1593,12 @@ msgstr ""
msgid "Copy commit SHA to clipboard"
msgstr ""
+msgid "Copy file name to clipboard"
+msgstr ""
+
+msgid "Copy file path to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
@@ -1606,6 +1623,9 @@ msgstr ""
msgid "Create branch"
msgstr ""
+msgid "Create commit"
+msgstr ""
+
msgid "Create directory"
msgstr ""
@@ -1863,6 +1883,9 @@ msgstr ""
msgid "Diffs|No file name available"
msgstr ""
+msgid "Diffs|Something went wrong while fetching diff lines."
+msgstr ""
+
msgid "Directory name"
msgstr ""
@@ -1941,6 +1964,9 @@ msgstr ""
msgid "Email"
msgstr ""
+msgid "Email patch"
+msgstr ""
+
msgid "Emails"
msgstr ""
@@ -2037,9 +2063,6 @@ msgstr ""
msgid "Error Reporting and Logging"
msgstr ""
-msgid "Error checking branch data. Please try again."
-msgstr ""
-
msgid "Error committing changes. Please try again."
msgstr ""
@@ -2118,6 +2141,9 @@ msgstr ""
msgid "Expand"
msgstr ""
+msgid "Expand all"
+msgstr ""
+
msgid "Expand sidebar"
msgstr ""
@@ -2386,6 +2412,9 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide whitespace changes"
+msgstr ""
+
msgid "History"
msgstr ""
@@ -2446,6 +2475,9 @@ msgstr ""
msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
+msgid "Inline"
+msgstr ""
+
msgid "Install Runner on Kubernetes"
msgstr ""
@@ -2536,6 +2568,9 @@ msgstr ""
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
+msgid "LFS"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -2658,9 +2693,6 @@ msgstr ""
msgid "Locked to current projects"
msgstr ""
-msgid "Locked to this project"
-msgstr ""
-
msgid "Login"
msgstr ""
@@ -2691,9 +2723,6 @@ msgstr ""
msgid "Maximum git storage failures"
msgstr ""
-msgid "Maximum job timeout"
-msgstr ""
-
msgid "May"
msgstr ""
@@ -2721,6 +2750,24 @@ msgstr ""
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
+msgid "MergeRequests|Resolve this discussion in a new issue"
+msgstr ""
+
+msgid "MergeRequests|Saving the comment failed"
+msgstr ""
+
+msgid "MergeRequests|Toggle comments for this file"
+msgstr ""
+
+msgid "MergeRequests|Updating discussions failed"
+msgstr ""
+
+msgid "MergeRequests|View file @ %{commitId}"
+msgstr ""
+
+msgid "MergeRequests|View replaced file @ %{commitId}"
+msgstr ""
+
msgid "Merged"
msgstr ""
@@ -2769,6 +2816,9 @@ msgstr ""
msgid "Monitoring"
msgstr ""
+msgid "More actions"
+msgstr ""
+
msgid "More information"
msgstr ""
@@ -2873,6 +2923,9 @@ msgstr ""
msgid "No file chosen"
msgstr ""
+msgid "No files found"
+msgstr ""
+
msgid "No files found."
msgstr ""
@@ -3002,6 +3055,9 @@ msgstr ""
msgid "Online IDE integration settings."
msgstr ""
+msgid "Only comments from the following commit are shown below"
+msgstr ""
+
msgid "Only project members can comment."
msgstr ""
@@ -3224,6 +3280,9 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "Plain diff"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
@@ -3521,12 +3580,6 @@ msgstr ""
msgid "Real-time features"
msgstr ""
-msgid "RefSwitcher|Branches"
-msgstr ""
-
-msgid "RefSwitcher|Tags"
-msgstr ""
-
msgid "Reference:"
msgstr ""
@@ -3605,6 +3658,9 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve all discussions in new issue"
+msgstr ""
+
msgid "Resolve conflicts on source branch"
msgstr ""
@@ -3814,17 +3870,29 @@ msgstr ""
msgid "Show complete raw log"
msgstr ""
+msgid "Show latest version"
+msgstr ""
+
+msgid "Show latest version of the diff"
+msgstr ""
+
msgid "Show parent pages"
msgstr ""
msgid "Show parent subgroups"
msgstr ""
+msgid "Show whitespace changes"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
+msgid "Side-by-side"
+msgstr ""
+
msgid "Sign out"
msgstr ""
@@ -3846,15 +3914,27 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong on our end. Please try again!"
+msgstr ""
+
msgid "Something went wrong when toggling the button"
msgstr ""
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3978,7 +4058,7 @@ msgstr ""
msgid "Stage"
msgstr ""
-msgid "Stage all"
+msgid "Stage all changes"
msgstr ""
msgid "Stage changes"
@@ -4259,6 +4339,9 @@ msgstr ""
msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
msgstr ""
+msgid "This diff is collapsed."
+msgstr ""
+
msgid "This directory"
msgstr ""
@@ -4331,6 +4414,9 @@ msgstr ""
msgid "This repository"
msgstr ""
+msgid "This source diff could not be displayed because it is too large."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr ""
@@ -4539,6 +4625,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Too many changes to show."
+msgstr ""
+
msgid "Total Time"
msgstr ""
@@ -4569,7 +4658,7 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
-msgid "Unstage all"
+msgid "Unstage all changes"
msgstr ""
msgid "Unstage changes"
@@ -4872,6 +4961,9 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
+msgid "You can %{linkStart}view the blob%{linkEnd} instead."
+msgstr ""
+
msgid "You can also create a project from the command line."
msgstr ""
@@ -5027,6 +5119,9 @@ msgstr ""
msgid "importing"
msgstr ""
+msgid "latest version"
+msgstr ""
+
msgid "merge request"
msgid_plural "merge requests"
msgstr[0] ""
diff --git a/package.json b/package.json
index 06b07c37d2b..c42bbbb0351 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
"karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
"karma-start": "BABEL_ENV=karma karma start config/karma.config.js",
+ "postinstall": "node ./scripts/frontend/postinstall.js",
"prettier-staged": "node ./scripts/frontend/prettier.js",
"prettier-staged-save": "node ./scripts/frontend/prettier.js save",
"prettier-all": "node ./scripts/frontend/prettier.js check-all",
@@ -17,7 +18,7 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
- "@gitlab-org/gitlab-svgs": "^1.23.0",
+ "@gitlab-org/gitlab-svgs": "^1.24.0",
"autosize": "^4.0.0",
"axios": "^0.17.1",
"babel-core": "^6.26.3",
diff --git a/qa/qa.rb b/qa/qa.rb
index 503379823f4..5013024e60f 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -46,10 +46,13 @@ module QA
autoload :Runner, 'qa/factory/resource/runner'
autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster'
+ autoload :Wiki, 'qa/factory/resource/wiki'
end
module Repository
autoload :Push, 'qa/factory/repository/push'
+ autoload :ProjectPush, 'qa/factory/repository/project_push'
+ autoload :WikiPush, 'qa/factory/repository/wiki_push'
end
module Settings
@@ -165,6 +168,16 @@ module QA
autoload :Show, 'qa/page/project/operations/kubernetes/show'
end
end
+
+ module Wiki
+ autoload :Edit, 'qa/page/project/wiki/edit'
+ autoload :New, 'qa/page/project/wiki/new'
+ autoload :Show, 'qa/page/project/wiki/show'
+ end
+ end
+
+ module Shared
+ autoload :ClonePanel, 'qa/page/shared/clone_panel'
end
module Profile
diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb
new file mode 100644
index 00000000000..48674c08a8d
--- /dev/null
+++ b/qa/qa/factory/repository/project_push.rb
@@ -0,0 +1,34 @@
+module QA
+ module Factory
+ module Repository
+ class ProjectPush < Factory::Repository::Push
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-with-code'
+ project.description = 'Project with repository'
+ end
+
+ product :output do |factory|
+ factory.output
+ end
+
+ def initialize
+ @file_name = 'file.txt'
+ @file_content = '# This is test project'
+ @commit_message = "This is a test commit"
+ @branch_name = 'master'
+ @new_branch = true
+ end
+
+ def repository_uri
+ @repository_uri ||= begin
+ project.visit!
+ Page::Project::Show.act do
+ choose_repository_clone_http
+ repository_location.uri
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb
index 7c0d580c5ca..4f97e65b091 100644
--- a/qa/qa/factory/repository/push.rb
+++ b/qa/qa/factory/repository/push.rb
@@ -5,25 +5,17 @@ module QA
module Repository
class Push < Factory::Base
attr_accessor :file_name, :file_content, :commit_message,
- :branch_name, :new_branch, :output
+ :branch_name, :new_branch, :output, :repository_uri
attr_writer :remote_branch
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-code'
- project.description = 'Project with repository'
- end
-
- product :output do |factory|
- factory.output
- end
-
def initialize
@file_name = 'file.txt'
- @file_content = '# This is test project'
+ @file_content = '# This is test file'
@commit_message = "This is a test commit"
@branch_name = 'master'
@new_branch = true
+ @repository_uri = ""
end
def remote_branch
@@ -37,14 +29,8 @@ module QA
end
def fabricate!
- project.visit!
-
Git::Repository.perform do |repository|
- repository.uri = Page::Project::Show.act do
- choose_repository_clone_http
- repository_location.uri
- end
-
+ repository.uri = repository_uri
repository.use_default_credentials
repository.clone
repository.configure_identity('GitLab QA', 'root@gitlab.com')
diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/factory/repository/wiki_push.rb
new file mode 100644
index 00000000000..fb7c2bb660d
--- /dev/null
+++ b/qa/qa/factory/repository/wiki_push.rb
@@ -0,0 +1,32 @@
+module QA
+ module Factory
+ module Repository
+ class WikiPush < Factory::Repository::Push
+ dependency Factory::Resource::Wiki, as: :wiki do |wiki|
+ wiki.title = 'Home'
+ wiki.content = '# My First Wiki Content'
+ wiki.message = 'Update home'
+ end
+
+ def initialize
+ @file_name = 'Home.md'
+ @file_content = '# Welcome to My Wiki'
+ @commit_message = 'Updating Home Page'
+ @branch_name = 'master'
+ @new_branch = false
+ end
+
+ def repository_uri
+ @repository_uri ||= begin
+ wiki.visit!
+ Page::Project::Wiki::Show.act do
+ go_to_clone_repository
+ choose_repository_clone_http
+ repository_location.uri
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb
index 4cabe7eab45..7fb0633ec90 100644
--- a/qa/qa/factory/resource/branch.rb
+++ b/qa/qa/factory/resource/branch.rb
@@ -31,13 +31,13 @@ module QA
def fabricate!
project.visit!
- Factory::Repository::Push.fabricate! do |resource|
+ Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.file_name = 'kick-off.txt'
resource.commit_message = 'First commit'
end
- branch = Factory::Repository::Push.fabricate! do |resource|
+ branch = Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.file_name = 'README.md'
resource.commit_message = 'Add readme'
diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
index 7588ac5735d..24d3597d993 100644
--- a/qa/qa/factory/resource/merge_request.rb
+++ b/qa/qa/factory/resource/merge_request.rb
@@ -21,14 +21,14 @@ module QA
project.name = 'project-with-merge-request'
end
- dependency Factory::Repository::Push, as: :target do |push, factory|
+ dependency Factory::Repository::ProjectPush, as: :target do |push, factory|
factory.project.visit!
push.project = factory.project
push.branch_name = 'master'
push.remote_branch = factory.target_branch
end
- dependency Factory::Repository::Push, as: :source do |push, factory|
+ dependency Factory::Repository::ProjectPush, as: :source do |push, factory|
push.project = factory.project
push.branch_name = factory.target_branch
push.remote_branch = factory.source_branch
diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb
new file mode 100644
index 00000000000..cc200a512d5
--- /dev/null
+++ b/qa/qa/factory/resource/wiki.rb
@@ -0,0 +1,25 @@
+module QA
+ module Factory
+ module Resource
+ class Wiki < Factory::Base
+ attr_accessor :title, :content, :message
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-for-wikis'
+ project.description = 'project for adding wikis'
+ end
+
+ def fabricate!
+ Page::Menu::Side.act { click_wiki }
+ Page::Project::Wiki::New.perform do |page|
+ page.go_to_create_first_page
+ page.set_title(@title)
+ page.set_content(@content)
+ page.set_message(@message)
+ page.create_new_page
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb
index 3630b7e8568..6bf4825cf00 100644
--- a/qa/qa/page/menu/side.rb
+++ b/qa/qa/page/menu/side.rb
@@ -13,6 +13,7 @@ module QA
element :top_level_items, '.sidebar-top-level-items'
element :operations_section, "class: 'shortcuts-operations'"
element :activity_link, "title: 'Activity'"
+ element :wiki_link_text, "Wiki"
end
view 'app/assets/javascripts/fly_out_nav.js' do
@@ -61,6 +62,12 @@ module QA
end
end
+ def click_wiki
+ within_sidebar do
+ click_link('Wiki')
+ end
+ end
+
private
def hover_settings
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index 4428e263bbb..a8558d7c50a 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -57,6 +57,10 @@ module QA
private
def within_project_deploy_keys
+ wait(reload: false) do
+ find_element(:project_deploy_keys)
+ end
+
within_element(:project_deploy_keys) do
yield
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 5bbef040330..1406edece17 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -2,11 +2,7 @@ module QA
module Page
module Project
class Show < Page::Base
- view 'app/views/shared/_clone_panel.html.haml' do
- element :clone_dropdown
- element :clone_options_dropdown, '.clone-options-dropdown'
- element :project_repository_location, 'text_field_tag :project_clone'
- end
+ include Page::Shared::ClonePanel
view 'app/views/projects/_last_push.html.haml' do
element :create_merge_request
@@ -26,21 +22,6 @@ module QA
element :branches_dropdown
end
- def choose_repository_clone_http
- choose_repository_clone('HTTP', 'http')
- end
-
- def choose_repository_clone_ssh
- # It's not always beginning with ssh:// so detecting with @
- # would be more reliable because ssh would always contain it.
- # We can't use .git because HTTP also contain that part.
- choose_repository_clone('SSH', '@')
- end
-
- def repository_location
- Git::Location.new(find('#project_clone').value)
- end
-
def project_name
find('.qa-project-name').text
end
@@ -65,31 +46,11 @@ module QA
click_element :create_merge_request
end
- def wait_for_push
- sleep 5
- refresh
- end
-
def go_to_new_issue
click_element :new_menu_toggle
click_link 'New issue'
end
-
- private
-
- def choose_repository_clone(kind, detect_text)
- wait(reload: false) do
- click_element :clone_dropdown
-
- page.within('.clone-options-dropdown') do
- click_link(kind)
- end
-
- # Ensure git clone textbox was updated
- repository_location.git_uri.include?(detect_text)
- end
- end
end
end
end
diff --git a/qa/qa/page/project/wiki/edit.rb b/qa/qa/page/project/wiki/edit.rb
new file mode 100644
index 00000000000..6fa45569cc0
--- /dev/null
+++ b/qa/qa/page/project/wiki/edit.rb
@@ -0,0 +1,27 @@
+module QA
+ module Page
+ module Project
+ module Wiki
+ class Edit < Page::Base
+ view 'app/views/projects/wikis/_main_links.html.haml' do
+ element :new_page_link, 'New page'
+ element :page_history_link, 'Page history'
+ element :edit_page_link, 'Edit'
+ end
+
+ def go_to_new_page
+ click_on 'New page'
+ end
+
+ def got_to_view_history_page
+ click_on 'Page history'
+ end
+
+ def go_to_edit_page
+ click_on 'Edit'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/wiki/new.rb b/qa/qa/page/project/wiki/new.rb
new file mode 100644
index 00000000000..415b3835538
--- /dev/null
+++ b/qa/qa/page/project/wiki/new.rb
@@ -0,0 +1,45 @@
+module QA
+ module Page
+ module Project
+ module Wiki
+ class New < Page::Base
+ view 'app/views/projects/wikis/_form.html.haml' do
+ element :wiki_title_textbox, 'text_field :title'
+ element :wiki_content_textarea, "render 'projects/zen', f: f, attr: :content"
+ element :wiki_message_textbox, 'text_field :message'
+ element :save_changes_button, 'submit _("Save changes")'
+ element :create_page_button, 'submit s_("Wiki|Create page")'
+ end
+
+ view 'app/views/shared/empty_states/_wikis.html.haml' do
+ element :create_link, 'Create your first page'
+ end
+
+ def go_to_create_first_page
+ click_link 'Create your first page'
+ end
+
+ def set_title(title)
+ fill_in 'wiki_title', with: title
+ end
+
+ def set_content(content)
+ fill_in 'wiki_content', with: content
+ end
+
+ def set_message(message)
+ fill_in 'wiki_message', with: message
+ end
+
+ def save_changes
+ click_on 'Save changes'
+ end
+
+ def create_new_page
+ click_on 'Create page'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/wiki/show.rb b/qa/qa/page/project/wiki/show.rb
new file mode 100644
index 00000000000..044e514bab3
--- /dev/null
+++ b/qa/qa/page/project/wiki/show.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Project
+ module Wiki
+ class Show < Page::Base
+ include Page::Shared::ClonePanel
+
+ view 'app/views/projects/wikis/pages.html.haml' do
+ element :clone_repository_link, 'Clone repository'
+ end
+
+ def go_to_clone_repository
+ click_on 'Clone repository'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/shared/clone_panel.rb b/qa/qa/page/shared/clone_panel.rb
new file mode 100644
index 00000000000..73e3dff956d
--- /dev/null
+++ b/qa/qa/page/shared/clone_panel.rb
@@ -0,0 +1,50 @@
+module QA
+ module Page
+ module Shared
+ module ClonePanel
+ def self.included(base)
+ base.view 'app/views/shared/_clone_panel.html.haml' do
+ element :clone_dropdown
+ element :clone_options_dropdown, '.clone-options-dropdown'
+ element :project_repository_location, 'text_field_tag :project_clone'
+ end
+ end
+
+ def choose_repository_clone_http
+ choose_repository_clone('HTTP', 'http')
+ end
+
+ def choose_repository_clone_ssh
+ # It's not always beginning with ssh:// so detecting with @
+ # would be more reliable because ssh would always contain it.
+ # We can't use .git because HTTP also contain that part.
+ choose_repository_clone('SSH', '@')
+ end
+
+ def repository_location
+ Git::Location.new(find('#project_clone').value)
+ end
+
+ def wait_for_push
+ sleep 5
+ refresh
+ end
+
+ private
+
+ def choose_repository_clone(kind, detect_text)
+ wait(reload: false) do
+ click_element :clone_dropdown
+
+ page.within('.clone-options-dropdown') do
+ click_link(kind)
+ end
+
+ # Ensure git clone textbox was updated
+ repository_location.git_uri.include?(detect_text)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 81d00d45753..2126ce6b234 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -3,6 +3,8 @@ module QA
module Env
extend self
+ attr_writer :user_type
+
# set to 'false' to have Chrome run visibly instead of headless
def chrome_headless?
(ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0
@@ -20,7 +22,9 @@ module QA
# By default, "standard" denotes a standard GitLab user login.
# Set this to "ldap" if the user should be logged in via LDAP.
def user_type
- (ENV['GITLAB_USER_TYPE'] || 'standard').tap do |type|
+ return @user_type if defined?(@user_type) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
+ ENV.fetch('GITLAB_USER_TYPE', 'standard').tap do |type|
unless %w(ldap standard).include?(type)
raise ArgumentError.new("Invalid user type '#{type}': must be 'ldap' or 'standard'")
end
diff --git a/qa/qa/specs/features/login/ldap_spec.rb b/qa/qa/specs/features/login/ldap_spec.rb
index ac2bd5a3c39..737f4d10053 100644
--- a/qa/qa/specs/features/login/ldap_spec.rb
+++ b/qa/qa/specs/features/login/ldap_spec.rb
@@ -1,8 +1,12 @@
module QA
feature 'LDAP user login', :ldap do
+ before do
+ Runtime::Env.user_type = 'ldap'
+ end
+
scenario 'user logs in using LDAP credentials' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_ldap_credentials }
+ Page::Main::Login.act { sign_in_using_credentials }
# TODO, since `Signed in successfully` message was removed
# this is the only way to tell if user is signed in correctly.
diff --git a/qa/qa/specs/features/merge_request/rebase_spec.rb b/qa/qa/specs/features/merge_request/rebase_spec.rb
index 2a44d42af6f..6a0ed4592c4 100644
--- a/qa/qa/specs/features/merge_request/rebase_spec.rb
+++ b/qa/qa/specs/features/merge_request/rebase_spec.rb
@@ -16,7 +16,7 @@ module QA
merge_request.title = 'Needs rebasing'
end
- Factory::Repository::Push.fabricate! do |push|
+ Factory::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = "other.txt"
push.file_content = "New file added!"
diff --git a/qa/qa/specs/features/merge_request/squash_spec.rb b/qa/qa/specs/features/merge_request/squash_spec.rb
index dbbdf852a38..b68704154cf 100644
--- a/qa/qa/specs/features/merge_request/squash_spec.rb
+++ b/qa/qa/specs/features/merge_request/squash_spec.rb
@@ -13,7 +13,7 @@ module QA
merge_request.title = 'Squashing commits'
end
- Factory::Repository::Push.fabricate! do |push|
+ Factory::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.commit_message = 'to be squashed'
push.branch_name = merge_request.source_branch
diff --git a/qa/qa/specs/features/project/activity_spec.rb b/qa/qa/specs/features/project/activity_spec.rb
index ba94ce8cf28..07ac7321aa2 100644
--- a/qa/qa/specs/features/project/activity_spec.rb
+++ b/qa/qa/specs/features/project/activity_spec.rb
@@ -4,7 +4,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Repository::Push.fabricate! do |push|
+ Factory::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
diff --git a/qa/qa/specs/features/project/auto_devops_spec.rb b/qa/qa/specs/features/project/auto_devops_spec.rb
index 202a847d1a5..c50a13432f5 100644
--- a/qa/qa/specs/features/project/auto_devops_spec.rb
+++ b/qa/qa/specs/features/project/auto_devops_spec.rb
@@ -16,7 +16,7 @@ module QA
end
# Create Auto Devops compatible repo
- Factory::Repository::Push.fabricate! do |push|
+ Factory::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.directory = Pathname
.new(__dir__)
diff --git a/qa/qa/specs/features/project/deploy_key_clone_spec.rb b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
index 46b3e38c1c5..10e4cbb6906 100644
--- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb
+++ b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
@@ -75,7 +75,7 @@ module QA
- docker
YAML
- Factory::Repository::Push.fabricate! do |resource|
+ Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = @project
resource.file_name = '.gitlab-ci.yml'
resource.commit_message = 'Add .gitlab-ci.yml'
diff --git a/qa/qa/specs/features/project/pipelines_spec.rb b/qa/qa/specs/features/project/pipelines_spec.rb
index 74f6474443d..bdb3d671516 100644
--- a/qa/qa/specs/features/project/pipelines_spec.rb
+++ b/qa/qa/specs/features/project/pipelines_spec.rb
@@ -40,7 +40,7 @@ module QA
runner.tags = %w[qa test]
end
- Factory::Repository::Push.fabricate! do |push|
+ Factory::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = '.gitlab-ci.yml'
push.commit_message = 'Add .gitlab-ci.yml'
diff --git a/qa/qa/specs/features/project/wikis_spec.rb b/qa/qa/specs/features/project/wikis_spec.rb
new file mode 100644
index 00000000000..49290a1a896
--- /dev/null
+++ b/qa/qa/specs/features/project/wikis_spec.rb
@@ -0,0 +1,45 @@
+module QA
+ feature 'Wiki Functionality', :core do
+ def login
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+ end
+
+ def validate_content(content)
+ expect(page).to have_content('Wiki was successfully updated')
+ expect(page).to have_content(/#{content}/)
+ end
+
+ before do
+ login
+ end
+
+ scenario 'User creates, edits, clones, and pushes to the wiki' do
+ wiki = Factory::Resource::Wiki.fabricate! do |resource|
+ resource.title = 'Home'
+ resource.content = '# My First Wiki Content'
+ resource.message = 'Update home'
+ end
+
+ validate_content('My First Wiki Content')
+
+ Page::Project::Wiki::Edit.act { go_to_edit_page }
+ Page::Project::Wiki::New.perform do |page|
+ page.set_content("My Second Wiki Content")
+ page.save_changes
+ end
+
+ validate_content('My Second Wiki Content')
+
+ Factory::Repository::WikiPush.fabricate! do |push|
+ push.wiki = wiki
+ push.file_name = 'Home.md'
+ push.file_content = '# My Third Wiki Content'
+ push.commit_message = 'Update Home.md'
+ end
+ Page::Menu::Side.act { click_wiki }
+
+ expect(page).to have_content('My Third Wiki Content')
+ end
+ end
+end
diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb
index 491675875b9..c5b8c271d7d 100644
--- a/qa/qa/specs/features/repository/protected_branches_spec.rb
+++ b/qa/qa/specs/features/repository/protected_branches_spec.rb
@@ -13,11 +13,15 @@ module QA
Page::Main::Login.act { sign_in_using_credentials }
end
- after do
+ after do |example|
# We need to clear localStorage because we're using it for the dropdown,
# and capybara doesn't do this for us.
# https://github.com/teamcapybara/capybara/issues/1702
Capybara.execute_script 'localStorage.clear()'
+
+ # In order to help diagnose a false failure
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/48241
+ log_push_output if example.exception
end
context 'when developers and maintainers are allowed to push to a protected branch' do
@@ -27,9 +31,9 @@ module QA
expect(protected_branch.name).to have_content(branch_name)
expect(protected_branch.push_allowance).to have_content('Developers + Maintainers')
- push = push_new_file(branch_name)
+ @push = push_new_file(branch_name)
- expect(push.output).to match(/remote: To create a merge request for protected-branch, visit/)
+ expect(@push.output).to match(/remote: To create a merge request for protected-branch, visit/)
end
end
@@ -37,11 +41,11 @@ module QA
scenario 'user without push rights fails to push to the protected branch' do
create_protected_branch(allow_to_push: false)
- push = push_new_file(branch_name)
+ @push = push_new_file(branch_name)
- expect(push.output)
+ expect(@push.output)
.to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/)
- expect(push.output)
+ expect(@push.output)
.to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/)
end
end
@@ -56,7 +60,7 @@ module QA
end
def push_new_file(branch)
- Factory::Repository::Push.fabricate! do |resource|
+ Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.file_name = 'new_file.md'
resource.file_content = '# This is a new file'
@@ -65,5 +69,13 @@ module QA
resource.new_branch = false
end
end
+
+ def log_push_output
+ if defined?(@push)
+ filename = File.join('tmp', "push-output-#{project.name}")
+ puts "Exception detected. Push output will be saved to #{filename}"
+ IO.binwrite(filename, @push.output)
+ end
+ end
end
end
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
index 51d9c2c7fd2..16aaa2e6762 100644
--- a/qa/qa/specs/features/repository/push_spec.rb
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -5,7 +5,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Repository::Push.fabricate! do |push|
+ Factory::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
diff --git a/rubocop/cop/gitlab/finder_with_find_by.rb b/rubocop/cop/gitlab/finder_with_find_by.rb
new file mode 100644
index 00000000000..f45a37ddc06
--- /dev/null
+++ b/rubocop/cop/gitlab/finder_with_find_by.rb
@@ -0,0 +1,52 @@
+module RuboCop
+ module Cop
+ module Gitlab
+ class FinderWithFindBy < RuboCop::Cop::Cop
+ FIND_PATTERN = /\Afind(_by\!?)?\z/
+ ALLOWED_MODULES = ['FinderMethods'].freeze
+
+ def message(used_method)
+ <<~MSG
+ Don't chain finders `#execute` method with `##{used_method}`.
+ Instead include `FinderMethods` in the Finder and call `##{used_method}`
+ directly on the finder instance.
+
+ This will make sure all authorization checks are performed on the resource.
+ MSG
+ end
+
+ def on_send(node)
+ if find_on_execute?(node) && !allowed_module?(node)
+ add_offense(node, location: :selector, message: message(node.method_name))
+ end
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ upto_including_execute = node.descendants.first.source_range
+ before_execute = node.descendants[1].source_range
+ range_to_remove = node.source_range
+ .with(begin_pos: before_execute.end_pos,
+ end_pos: upto_including_execute.end_pos)
+
+ corrector.remove(range_to_remove)
+ end
+ end
+
+ def find_on_execute?(node)
+ chained_on_node = node.descendants.first
+ node.method_name.to_s =~ FIND_PATTERN &&
+ chained_on_node&.method_name == :execute
+ end
+
+ def allowed_module?(node)
+ ALLOWED_MODULES.include?(node.parent_module_name)
+ end
+
+ def method_name_for_node(node)
+ children[1].to_s
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/update_large_table.rb b/rubocop/cop/migration/update_large_table.rb
index bb14d0f4f56..c15eec22d04 100644
--- a/rubocop/cop/migration/update_large_table.rb
+++ b/rubocop/cop/migration/update_large_table.rb
@@ -20,10 +20,14 @@ module RuboCop
'necessary'.freeze
LARGE_TABLES = %i[
- ci_pipelines
+ ci_build_trace_sections
ci_builds
+ ci_job_artifacts
+ ci_pipelines
+ ci_stages
events
issues
+ merge_request_diff_commits
merge_request_diff_files
merge_request_diffs
merge_requests
@@ -34,8 +38,15 @@ module RuboCop
users
].freeze
+ BATCH_UPDATE_METHODS = %w[
+ :add_column_with_default
+ :change_column_type_concurrently
+ :rename_column_concurrently
+ :update_column_in_batches
+ ].join(' ').freeze
+
def_node_matcher :batch_update?, <<~PATTERN
- (send nil? ${:add_column_with_default :update_column_in_batches} $(sym ...) ...)
+ (send nil? ${#{BATCH_UPDATE_METHODS}} $(sym ...) ...)
PATTERN
def on_send(node)
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index f05990232ab..aa7ae601f75 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -2,6 +2,7 @@
require_relative 'cop/gitlab/module_with_instance_variables'
require_relative 'cop/gitlab/predicate_memoization'
require_relative 'cop/gitlab/httparty'
+require_relative 'cop/gitlab/finder_with_find_by'
require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/avoid_return_from_blocks'
require_relative 'cop/avoid_break_from_strong_memoize'
diff --git a/scripts/frontend/postinstall.js b/scripts/frontend/postinstall.js
new file mode 100644
index 00000000000..682039a41b3
--- /dev/null
+++ b/scripts/frontend/postinstall.js
@@ -0,0 +1,22 @@
+const chalk = require('chalk');
+
+// check that fsevents is available if we're on macOS
+if (process.platform === 'darwin') {
+ try {
+ require.resolve('fsevents');
+ } catch (e) {
+ console.error(`${chalk.red('error')} Dependency postinstall check failed.`);
+ console.error(
+ chalk.red(`
+ The fsevents driver is not installed properly.
+ If you are running a new version of Node, please
+ ensure that it is supported by the fsevents library.
+
+ You can try installing again with \`${chalk.cyan('yarn install --force')}\`
+ `)
+ );
+ process.exit(1);
+ }
+}
+
+console.log(`${chalk.green('success')} Dependency postinstall check passed.`);
diff --git a/scripts/trigger-build b/scripts/trigger-build
index 526f5164ede..798bf1e82b7 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -165,6 +165,10 @@ module Trigger
end
JSON.parse(res.body)['status'].to_s.to_sym
+ rescue JSON::ParserError
+ # Ignore GitLab API hiccups. If GitLab is really down, we'll hit the job
+ # timeout anyway.
+ :running
end
end
end
diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs
index c9aaba91aa0..2a0e7f4d76e 100755
--- a/scripts/trigger-build-docs
+++ b/scripts/trigger-build-docs
@@ -27,7 +27,7 @@ def docs_branch
# Prefix the remote branch with the slug of the project in order
# to avoid name conflicts in the rare case the branch name already
# exists in the docs repo and truncate to max length.
- "#{slug}-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max]
+ "#{slug}-#{ENV["CI_ENVIRONMENT_SLUG"]}"[0...max]
end
#
diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
index fc1bf67d7b9..f278043028f 100644
--- a/spec/bin/changelog_spec.rb
+++ b/spec/bin/changelog_spec.rb
@@ -56,11 +56,11 @@ describe 'bin/changelog' do
it 'parses -h' do
expect do
expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout
- end.to raise_error(SystemExit)
+ end.to raise_error(ChangelogHelpers::Done)
end
it 'assigns title' do
- options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend])
+ options = described_class.parse(%W[foo -m 1 bar\n baz\r\n --amend])
expect(options.title).to eq 'foo bar baz'
end
@@ -82,9 +82,10 @@ describe 'bin/changelog' do
it 'shows error message and exits the program' do
allow($stdin).to receive(:getc).and_return(type)
expect do
- expect do
- expect { described_class.read_type }.to raise_error(SystemExit)
- end.to output("Invalid category index, please select an index between 1 and 8\n").to_stderr
+ expect { described_class.read_type }.to raise_error(
+ ChangelogHelpers::Abort,
+ 'Invalid category index, please select an index between 1 and 8'
+ )
end.to output.to_stdout
end
end
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 5f0e8c5eca9..b23f183fec8 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -1,127 +1,162 @@
require 'spec_helper'
-describe OmniauthCallbacksController do
+describe OmniauthCallbacksController, type: :controller do
include LoginHelpers
- let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
-
- before do
- mock_auth_hash(provider.to_s, extern_uid, user.email)
- stub_omniauth_provider(provider, context: request)
- end
-
- context 'when the user is on the last sign in attempt' do
- let(:extern_uid) { 'my-uid' }
+ describe 'omniauth' do
+ let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
before do
- user.update(failed_attempts: User.maximum_attempts.pred)
- subject.response = ActionDispatch::Response.new
+ mock_auth_hash(provider.to_s, extern_uid, user.email)
+ stub_omniauth_provider(provider, context: request)
end
- context 'when using a form based provider' do
- let(:provider) { :ldap }
-
- it 'locks the user when sign in fails' do
- allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username))
- request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil)
-
- subject.send(:failure)
+ context 'when the user is on the last sign in attempt' do
+ let(:extern_uid) { 'my-uid' }
- expect(user.reload).to be_access_locked
+ before do
+ user.update(failed_attempts: User.maximum_attempts.pred)
+ subject.response = ActionDispatch::Response.new
end
- end
- context 'when using a button based provider' do
- let(:provider) { :github }
+ context 'when using a form based provider' do
+ let(:provider) { :ldap }
- it 'does not lock the user when sign in fails' do
- request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil)
+ it 'locks the user when sign in fails' do
+ allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username))
+ request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil)
- subject.send(:failure)
+ subject.send(:failure)
- expect(user.reload).not_to be_access_locked
+ expect(user.reload).to be_access_locked
+ end
end
- end
- end
- context 'strategies' do
- context 'github' do
- let(:extern_uid) { 'my-uid' }
- let(:provider) { :github }
+ context 'when using a button based provider' do
+ let(:provider) { :github }
- it 'allows sign in' do
- post provider
+ it 'does not lock the user when sign in fails' do
+ request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil)
- expect(request.env['warden']).to be_authenticated
- end
-
- shared_context 'sign_up' do
- let(:user) { double(email: 'new@example.com') }
+ subject.send(:failure)
- before do
- stub_omniauth_setting(block_auto_created_users: false)
+ expect(user.reload).not_to be_access_locked
end
end
+ end
- context 'sign up' do
- include_context 'sign_up'
+ context 'strategies' do
+ context 'github' do
+ let(:extern_uid) { 'my-uid' }
+ let(:provider) { :github }
- it 'is allowed' do
+ it 'allows sign in' do
post provider
expect(request.env['warden']).to be_authenticated
end
- end
-
- context 'when OAuth is disabled' do
- before do
- stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- settings = Gitlab::CurrentSettings.current_application_settings
- settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
- end
- it 'prevents login via POST' do
- post provider
+ shared_context 'sign_up' do
+ let(:user) { double(email: 'new@example.com') }
- expect(request.env['warden']).not_to be_authenticated
+ before do
+ stub_omniauth_setting(block_auto_created_users: false)
+ end
end
- it 'shows warning when attempting login' do
- post provider
-
- expect(response).to redirect_to new_user_session_path
- expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
- end
+ context 'sign up' do
+ include_context 'sign_up'
- it 'allows linking the disabled provider' do
- user.identities.destroy_all
- sign_in(user)
+ it 'is allowed' do
+ post provider
- expect { post provider }.to change { user.reload.identities.count }.by(1)
+ expect(request.env['warden']).to be_authenticated
+ end
end
- context 'sign up' do
- include_context 'sign_up'
+ context 'when OAuth is disabled' do
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ settings = Gitlab::CurrentSettings.current_application_settings
+ settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
+ end
- it 'is prevented' do
+ it 'prevents login via POST' do
post provider
expect(request.env['warden']).not_to be_authenticated
end
+
+ it 'shows warning when attempting login' do
+ post provider
+
+ expect(response).to redirect_to new_user_session_path
+ expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
+ end
+
+ it 'allows linking the disabled provider' do
+ user.identities.destroy_all
+ sign_in(user)
+
+ expect { post provider }.to change { user.reload.identities.count }.by(1)
+ end
+
+ context 'sign up' do
+ include_context 'sign_up'
+
+ it 'is prevented' do
+ post provider
+
+ expect(request.env['warden']).not_to be_authenticated
+ end
+ end
+ end
+ end
+
+ context 'auth0' do
+ let(:extern_uid) { '' }
+ let(:provider) { :auth0 }
+
+ it 'does not allow sign in without extern_uid' do
+ post 'auth0'
+
+ expect(request.env['warden']).not_to be_authenticated
+ expect(response.status).to eq(302)
+ expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
end
end
end
+ end
+
+ describe '#saml' do
+ let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') }
+ let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
+ let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
+
+ before do
+ stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
+ providers: [saml_config] })
+ mock_auth_hash('saml', 'my-uid', user.email, mock_saml_response)
+ request.env["devise.mapping"] = Devise.mappings[:user]
+ request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
+ post :saml, params: { SAMLResponse: mock_saml_response }
+ end
- context 'auth0' do
- let(:extern_uid) { '' }
- let(:provider) { :auth0 }
+ context 'when worth two factors' do
+ let(:mock_saml_response) do
+ File.read('spec/fixtures/authentication/saml_response.xml')
+ .gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN')
+ end
- it 'does not allow sign in without extern_uid' do
- post 'auth0'
+ it 'expects user to be signed_in' do
+ expect(request.env['warden']).to be_authenticated
+ end
+ end
+ context 'when not worth two factors' do
+ it 'expects user to provide second factor' do
+ expect(response).to render_template('devise/sessions/two_factor')
expect(request.env['warden']).not_to be_authenticated
- expect(response.status).to eq(302)
- expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 9e696e9cb29..4dcb7dc6c87 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -122,10 +122,64 @@ describe Projects::BlobController do
end
context 'when essential params are present' do
- it 'renders the diff content' do
- do_get(since: 1, to: 5, offset: 10)
+ context 'when rendering for commit' do
+ it 'renders the diff content' do
+ do_get(since: 1, to: 5, offset: 10)
- expect(response.body).to be_present
+ expect(response.body).to be_present
+ end
+ end
+
+ context 'when rendering for merge request' do
+ it 'renders diff context lines Gitlab::Diff::Line array' do
+ do_get(since: 1, to: 5, offset: 10, from_merge_request: true)
+
+ lines = JSON.parse(response.body)
+
+ expect(lines.first).to have_key('type')
+ expect(lines.first).to have_key('rich_text')
+ expect(lines.first).to have_key('rich_text')
+ end
+
+ context 'when rendering match lines' do
+ it 'adds top match line when "since" is less than 1' do
+ do_get(since: 5, to: 10, offset: 10, from_merge_request: true)
+
+ match_line = JSON.parse(response.body).first
+
+ expect(match_line['type']).to eq('match')
+ expect(match_line['meta_data']).to have_key('old_pos')
+ expect(match_line['meta_data']).to have_key('new_pos')
+ end
+
+ it 'does not add top match line when when "since" is equal 1' do
+ do_get(since: 1, to: 10, offset: 10, from_merge_request: true)
+
+ match_line = JSON.parse(response.body).first
+
+ expect(match_line['type']).to eq('context')
+ end
+
+ it 'adds bottom match line when "t"o is less than blob size' do
+ do_get(since: 1, to: 5, offset: 10, from_merge_request: true, bottom: true)
+
+ match_line = JSON.parse(response.body).last
+
+ expect(match_line['type']).to eq('match')
+ expect(match_line['meta_data']).to have_key('old_pos')
+ expect(match_line['meta_data']).to have_key('new_pos')
+ end
+
+ it 'does not add bottom match line when "to" is less than blob size' do
+ commit_id = project.repository.commit('master').id
+ blob = project.repository.blob_at(commit_id, 'CHANGELOG')
+ do_get(since: 1, to: blob.lines.count, offset: 10, from_merge_request: true, bottom: true)
+
+ match_line = JSON.parse(response.body).last
+
+ expect(match_line['type']).to eq('context')
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index 53647749a60..4aa33dbbb01 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -110,7 +110,7 @@ describe Projects::DiscussionsController do
it "returns the name of the resolving user" do
post :resolve, request_params
- expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name)
+ expect(JSON.parse(response.body)['resolved_by']['name']).to eq(user.name)
end
it "returns status 200" do
@@ -119,16 +119,21 @@ describe Projects::DiscussionsController do
expect(response).to have_gitlab_http_status(200)
end
- context "when vue_mr_discussions cookie is present" do
- before do
- allow(controller).to receive(:cookies).and_return(vue_mr_discussions: 'true')
- end
+ it "renders discussion with serializer" do
+ expect_any_instance_of(DiscussionSerializer).to receive(:represent)
+ .with(instance_of(Discussion), { context: instance_of(described_class), render_truncated_diff_lines: true })
- it "renders discussion with serializer" do
- expect_any_instance_of(DiscussionSerializer).to receive(:represent)
- .with(instance_of(Discussion), { context: instance_of(described_class) })
+ post :resolve, request_params
+ end
+ context 'diff discussion' do
+ let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+ let(:discussion) { note.discussion }
+
+ it "returns truncated diff lines" do
post :resolve, request_params
+
+ expect(JSON.parse(response.body)['truncated_diff_lines']).to be_present
end
end
end
@@ -187,7 +192,7 @@ describe Projects::DiscussionsController do
it "renders discussion with serializer" do
expect_any_instance_of(DiscussionSerializer).to receive(:represent)
- .with(instance_of(Discussion), { context: instance_of(described_class) })
+ .with(instance_of(Discussion), { context: instance_of(described_class), render_truncated_diff_lines: true })
delete :unresolve, request_params
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 106611b37c9..3a41f0fc07a 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -990,7 +990,7 @@ describe Projects::IssuesController do
it 'returns discussion json' do
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
- expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion individual_note resolvable resolved])
+ expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion discussion_path individual_note resolvable resolved resolved_at resolved_by resolved_by_push commit_id for_commit project_id])
end
context 'with cross-reference system note', :request_store do
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 5d297c654bf..ec82b35f227 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -26,12 +26,13 @@ describe Projects::MergeRequests::DiffsController do
context 'with default params' do
context 'for the same project' do
before do
- go
+ allow(controller).to receive(:rendered_for_merge_request?).and_return(true)
end
- it 'renders the diffs template to a string' do
- expect(response).to render_template('projects/merge_requests/diffs/_diffs')
- expect(json_response).to have_key('html')
+ it 'serializes merge request diff collection' do
+ expect_any_instance_of(DiffsSerializer).to receive(:represent).with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), an_instance_of(Hash))
+
+ go
end
end
@@ -56,17 +57,6 @@ describe Projects::MergeRequests::DiffsController do
end
end
- context 'with ignore_whitespace_change' do
- before do
- go(w: 1)
- end
-
- it 'renders the diffs template to a string' do
- expect(response).to render_template('projects/merge_requests/diffs/_diffs')
- expect(json_response).to have_key('html')
- end
- end
-
context 'with view' do
before do
go(view: 'parallel')
@@ -105,12 +95,11 @@ describe Projects::MergeRequests::DiffsController do
end
it 'only renders the diffs for the path given' do
- expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
- expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
- meth.call(diffs)
- end
-
diff_for_path(old_path: existing_path, new_path: existing_path)
+
+ paths = JSON.parse(response.body)["diff_files"].map { |file| file['new_path'] }
+
+ expect(paths).to include(existing_path)
end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index de132dfaa21..1458113b90c 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -51,7 +51,7 @@ describe Projects::NotesController do
let(:project) { create(:project, :repository) }
let!(:note) { create(:discussion_note_on_merge_request, project: project) }
- let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) }
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id, html: true) }
it 'responds with the expected attributes' do
get :index, params
@@ -67,7 +67,7 @@ describe Projects::NotesController do
let(:project) { create(:project, :repository) }
let!(:note) { create(:diff_note_on_merge_request, project: project) }
- let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) }
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id, html: true) }
it 'responds with the expected attributes' do
get :index, params
@@ -86,7 +86,7 @@ describe Projects::NotesController do
context 'when displayed on a merge request' do
let(:merge_request) { create(:merge_request, source_project: project) }
- let(:params) { request_params.merge(target_type: 'merge_request', target_id: merge_request.id) }
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: merge_request.id, html: true) }
it 'responds with the expected attributes' do
get :index, params
@@ -99,7 +99,7 @@ describe Projects::NotesController do
end
context 'when displayed on the commit' do
- let(:params) { request_params.merge(target_type: 'commit', target_id: note.commit_id) }
+ let(:params) { request_params.merge(target_type: 'commit', target_id: note.commit_id, html: true) }
it 'responds with the expected attributes' do
get :index, params
@@ -128,7 +128,7 @@ describe Projects::NotesController do
context 'for a regular note' do
let!(:note) { create(:note_on_merge_request, project: project) }
- let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) }
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id, html: true) }
it 'responds with the expected attributes' do
get :index, params
@@ -293,7 +293,7 @@ describe Projects::NotesController do
context 'when a noteable is not found' do
it 'returns 404 status' do
- request_params[:note][:noteable_id] = 9999
+ request_params[:target_id] = 9999
post :create, request_params.merge(format: :json)
expect(response).to have_gitlab_http_status(404)
@@ -475,7 +475,7 @@ describe Projects::NotesController do
end
it "returns the name of the resolving user" do
- post :resolve, request_params
+ post :resolve, request_params.merge(html: true)
expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name)
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 90e698925b6..27f04be3fdf 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -329,7 +329,7 @@ describe ProjectsController do
expect { update_project path: 'renamed_path' }
.not_to change { project.reload.path }
- expect(controller).to set_flash[:alert].to(/container registry tags/)
+ expect(controller).to set_flash.now[:alert].to(/container registry tags/)
expect(response).to have_gitlab_http_status(200)
end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 2b61e0d4a85..cdec26bd421 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -53,21 +53,22 @@ describe SessionsController do
include UserActivitiesHelpers
let(:user) { create(:user) }
+ let(:user_params) { { login: user.username, password: user.password } }
it 'authenticates user correctly' do
- post(:create, user: { login: user.username, password: user.password })
+ post(:create, user: user_params)
expect(subject.current_user). to eq user
end
it 'creates an audit log record' do
- expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1)
+ expect { post(:create, user: user_params) }.to change { SecurityEvent.count }.by(1)
expect(SecurityEvent.last.details[:with]).to eq('standard')
end
include_examples 'user login request with unique ip limit', 302 do
def request
- post(:create, user: { login: user.username, password: user.password })
+ post(:create, user: user_params)
expect(subject.current_user).to eq user
subject.sign_out user
end
@@ -75,10 +76,40 @@ describe SessionsController do
it 'updates the user activity' do
expect do
- post(:create, user: { login: user.username, password: user.password })
+ post(:create, user: user_params)
end.to change { user_activity(user) }
end
end
+
+ context 'when reCAPTCHA is enabled' do
+ let(:user) { create(:user) }
+ let(:user_params) { { login: user.username, password: user.password } }
+
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ request.headers[described_class::CAPTCHA_HEADER] = 1
+ end
+
+ it 'displays an error when the reCAPTCHA is not solved' do
+ # Without this, `verify_recaptcha` arbitraily returns true in test env
+ Recaptcha.configuration.skip_verify_env.delete('test')
+
+ post(:create, user: user_params)
+
+ expect(response).to render_template(:new)
+ expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ expect(subject.current_user).to be_nil
+ end
+
+ it 'successfully logs in a user when reCAPTCHA is solved' do
+ # Avoid test ordering issue and ensure `verify_recaptcha` returns true
+ Recaptcha.configuration.skip_verify_env << 'test'
+
+ post(:create, user: user_params)
+
+ expect(subject.current_user).to eq user
+ end
+ end
end
context 'when using two-factor authentication via OTP' do
diff --git a/spec/dependencies/omniauth_saml_spec.rb b/spec/dependencies/omniauth_saml_spec.rb
new file mode 100644
index 00000000000..ccc604dc230
--- /dev/null
+++ b/spec/dependencies/omniauth_saml_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+require 'omniauth/strategies/saml'
+
+describe 'processing of SAMLResponse in dependencies' do
+ let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
+ let(:saml_strategy) { OmniAuth::Strategies::SAML.new({}) }
+ let(:session_mock) { {} }
+ let(:settings) { OpenStruct.new({ soft: false, idp_cert_fingerprint: 'something' }) }
+ let(:auth_hash) { Gitlab::Auth::Saml::AuthHash.new(saml_strategy) }
+
+ subject { auth_hash.authn_context }
+
+ before do
+ allow(saml_strategy).to receive(:session).and_return(session_mock)
+ allow_any_instance_of(OneLogin::RubySaml::Response).to receive(:is_valid?).and_return(true)
+ saml_strategy.send(:handle_response, mock_saml_response, {}, settings ) { }
+ end
+
+ it 'can extract AuthnContextClassRef from SAMLResponse param' do
+ is_expected.to eq 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
+ end
+end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index d5e603baeae..a4226d7a682 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -31,6 +31,7 @@ feature 'Admin Groups' do
path_component = 'gitlab'
group_name = 'GitLab group name'
group_description = 'Description of group for GitLab'
+
fill_in 'group_path', with: path_component
fill_in 'group_name', with: group_name
fill_in 'group_description', with: group_description
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index e7aca94db66..f3ab4ff771a 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -124,6 +124,29 @@ feature 'Admin updates settings' do
expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).not_to include('google_oauth2')
end
+ scenario 'Oauth providers do not raise validation errors when saving unrelated changes' do
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty
+
+ page.within('.as-signin') do
+ uncheck 'Google'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2')
+
+ # Remove google_oauth2 from the Omniauth strategies
+ allow(Devise).to receive(:omniauth_providers).and_return([])
+
+ # Save an unrelated setting
+ page.within('.as-ci-cd') do
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2')
+ end
+
scenario 'Change Help page' do
page.within('.as-help-page') do
fill_in 'Help page text', with: 'Example text'
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
index fa0ab88624e..8eaccfc0949 100644
--- a/spec/features/issuables/markdown_references/jira_spec.rb
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -163,7 +163,7 @@ describe "Jira", :js do
HEREDOC
page.within("#diff-notes-app") do
- fill_in("note_note", with: markdown)
+ fill_in("note-body", with: markdown)
end
end
diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb
index e25fd1a6249..0a19086ffbd 100644
--- a/spec/features/issuables/shortcuts_issuable_spec.rb
+++ b/spec/features/issuables/shortcuts_issuable_spec.rb
@@ -12,6 +12,15 @@ feature 'Blob shortcuts', :js do
sign_in(user)
end
+ shared_examples "quotes the selected text" do
+ it "quotes the selected text" do
+ select_element('.note-text')
+ find('body').native.send_key('r')
+
+ expect(find('.js-main-target-form .js-vue-comment-form').value).to include(note_text)
+ end
+ end
+
describe 'pressing "r"' do
describe 'On an Issue' do
before do
@@ -20,12 +29,7 @@ feature 'Blob shortcuts', :js do
wait_for_requests
end
- it 'quotes the selected text' do
- select_element('.note-text')
- find('body').native.send_key('r')
-
- expect(find('.js-main-target-form .js-vue-comment-form').value).to include(note_text)
- end
+ include_examples 'quotes the selected text'
end
describe 'On a Merge Request' do
@@ -35,12 +39,7 @@ feature 'Blob shortcuts', :js do
wait_for_requests
end
- it 'quotes the selected text' do
- select_element('.note-text')
- find('body').native.send_key('r')
-
- expect(find('.js-main-target-form #note_note').value).to include(note_text)
- end
+ include_examples 'quotes the selected text'
end
end
end
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 e0466aaf422..52962002c33 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
@@ -6,6 +6,12 @@ feature 'Resolving all open discussions in a merge request from an issue', :js d
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+ def resolve_all_discussions_link_selector
+ text = "Resolve all discussions in new issue"
+ url = new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ %Q{a[data-original-title="#{text}"][href="#{url}"]}
+ end
+
describe 'as a user with access to the project' do
before do
project.add_master(user)
@@ -14,8 +20,8 @@ feature 'Resolving all open discussions in a merge request from an issue', :js d
end
it 'shows a button to resolve all discussions by creating a new issue' do
- within('#resolve-count-app') do
- expect(page).to have_link "Resolve all discussions in new issue", href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ within('.line-resolve-all-container') do
+ expect(page).to have_selector resolve_all_discussions_link_selector
end
end
@@ -25,13 +31,13 @@ feature 'Resolving all open discussions in a merge request from an issue', :js d
end
it 'hides the link for creating a new issue' do
- expect(page).not_to have_link "Resolve all discussions in new issue", href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ expect(page).not_to have_selector resolve_all_discussions_link_selector
end
end
context 'creating an issue for discussions' do
before do
- click_link "Resolve all discussions in new issue", href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ find(resolve_all_discussions_link_selector).click
end
it_behaves_like 'creating an issue for a discussion'
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 34beb282bad..9170f9295f0 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
@@ -1,11 +1,17 @@
require 'rails_helper'
-feature 'Resolve an open discussion in a merge request by creating an issue' do
+feature 'Resolve an open discussion in a merge request by creating an issue', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+ def resolve_discussion_selector
+ title = 'Resolve this discussion in a new issue'
+ url = new_project_issue_path(project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
+ "a[data-original-title=\"#{title}\"][href=\"#{url}\"]"
+ end
+
describe 'As a user with access to the project' do
before do
project.add_master(user)
@@ -20,17 +26,17 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do
end
it 'does not show a link to create a new issue' do
- expect(page).not_to have_link 'Resolve this discussion in a new issue'
+ expect(page).not_to have_css resolve_discussion_selector
end
end
- context 'resolving the discussion', :js do
+ context 'resolving the discussion' do
before do
click_button 'Resolve discussion'
end
it 'hides the link for creating a new issue' do
- expect(page).not_to have_link 'Resolve this discussion in a new issue'
+ expect(page).not_to have_css resolve_discussion_selector
end
it 'shows the link for creating a new issue when unresolving a discussion' do
@@ -38,19 +44,17 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do
click_button 'Unresolve discussion'
end
- expect(page).to have_link 'Resolve this discussion in a new issue'
+ expect(page).to have_css resolve_discussion_selector
end
end
it 'has a link to create a new issue for a discussion' do
- new_issue_link = new_project_issue_path(project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
-
- expect(page).to have_link 'Resolve this discussion in a new issue', href: new_issue_link
+ expect(page).to have_css resolve_discussion_selector
end
context 'creating the issue' do
before do
- click_link 'Resolve this discussion in a new issue', href: new_project_issue_path(project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
+ find(resolve_discussion_selector).click
end
it 'has a hidden field for the discussion' do
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index bc42618306f..8dca81a8627 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -10,6 +10,7 @@ describe 'Filter issues', :js do
# When the name is longer, the filtered search input can end up scrolling
# horizontally, and PhantomJS can't handle it.
let(:user) { create(:user, name: 'Ann') }
+ let(:user2) { create(:user, name: 'jane') }
let!(:bug_label) { create(:label, project: project, title: 'bug') }
let!(:caps_sensitive_label) { create(:label, project: project, title: 'CaPs') }
@@ -25,8 +26,6 @@ describe 'Filter issues', :js do
before do
project.add_master(user)
- user2 = create(:user)
-
create(:issue, project: project, author: user2, title: "Bug report 1")
create(:issue, project: project, author: user2, title: "Bug report 2")
@@ -113,6 +112,24 @@ describe 'Filter issues', :js do
expect_issues_list_count(3)
expect_filtered_search_input_empty
end
+
+ it 'filters issues by invalid assignee' do
+ skip('to be tested, issue #26546')
+ end
+
+ it 'filters issues by multiple assignees' do
+ create(:issue, project: project, author: user, assignees: [user2, user])
+
+ input_filtered_search("assignee:@#{user.username} assignee:@#{user2.username}")
+
+ expect_tokens([
+ assignee_token(user.name),
+ assignee_token(user2.name)
+ ])
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input_empty
+ end
end
end
@@ -491,6 +508,21 @@ describe 'Filter issues', :js do
it_behaves_like 'updates atom feed link', :group do
let(:path) { issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) }
end
+
+ it 'updates atom feed link for group issues' do
+ visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id)
+ link = find('.nav-controls a[title="Subscribe to RSS feed"]', visible: false)
+ params = CGI.parse(URI.parse(link[:href]).query)
+ auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
+ auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
+
+ expect(params).to include('feed_token' => [user.feed_token])
+ expect(params).to include('milestone_title' => [milestone.title])
+ expect(params).to include('assignee_id' => [user.id.to_s])
+ expect(auto_discovery_params).to include('feed_token' => [user.feed_token])
+ expect(auto_discovery_params).to include('milestone_title' => [milestone.title])
+ expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+ end
end
context 'URL has a trailing slash' do
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index 25c408516d1..728e89db400 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -114,7 +114,8 @@ feature 'Merge request > User creates image diff notes', :js do
create_image_diff_note
end
- it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
indicator = find('.js-image-badge', match: :first)
badge = find('.image-diff-avatar-link .badge', match: :first)
@@ -156,7 +157,8 @@ feature 'Merge request > User creates image diff notes', :js do
visit project_merge_request_path(project, merge_request)
end
- it 'render diff indicators within the image frame' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit '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
diff --git a/spec/features/merge_request/user_locks_discussion_spec.rb b/spec/features/merge_request/user_locks_discussion_spec.rb
index a68df872334..76c759ab8d3 100644
--- a/spec/features/merge_request/user_locks_discussion_spec.rb
+++ b/spec/features/merge_request/user_locks_discussion_spec.rb
@@ -38,9 +38,9 @@ describe 'Merge request > User locks discussion', :js do
end
it 'the user can not create a comment' do
- page.within('.issuable-discussion #notes') do
+ page.within('.js-vue-notes-event') do
expect(page).not_to have_selector('js-main-target-form')
- expect(page.find('.disabled-comment'))
+ expect(page.find('.issuable-note-warning'))
.to have_content('This merge request is locked. Only project members can comment.')
end
end
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index 2b4623d6dc9..13cc5f256eb 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -65,11 +65,13 @@ describe 'Merge request > User posts diff notes', :js do
context 'with a match line' do
it 'does not allow commenting on the left side' do
- should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left')
+ line_holder = find('.match', match: :first).find(:xpath, '..')
+ should_not_allow_commenting(line_holder, 'left')
end
it 'does not allow commenting on the right side' do
- should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right')
+ line_holder = find('.match', match: :first).find(:xpath, '..')
+ should_not_allow_commenting(line_holder, 'right')
end
end
@@ -81,7 +83,7 @@ describe 'Merge request > User posts diff notes', :js do
# The first `.js-unfold` unfolds upwards, therefore the first
# `.line_holder` will be an unfolded line.
- let(:line_holder) { first('.line_holder[id="1"]') }
+ let(:line_holder) { first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder') }
it 'does not allow commenting on the left side' do
should_not_allow_commenting(line_holder, 'left')
@@ -143,7 +145,7 @@ describe 'Merge request > User posts diff notes', :js do
# The first `.js-unfold` unfolds upwards, therefore the first
# `.line_holder` will be an unfolded line.
- let(:line_holder) { first('.line_holder[id="1"]') }
+ let(:line_holder) { first('.line_holder[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') }
it 'does not allow commenting' do
should_not_allow_commenting line_holder
@@ -183,7 +185,7 @@ describe 'Merge request > User posts diff notes', :js do
end
describe 'posting a note' do
- it 'adds as discussion' do
+ xit 'adds as discussion' do
expect(page).to have_css('.js-temp-notes-holder', count: 2)
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false)
@@ -201,20 +203,23 @@ describe 'Merge request > User posts diff notes', :js do
end
context 'with a new line' do
- it 'allows commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'allows commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]').find(:xpath, '..'))
end
end
context 'with an old line' do
- it 'allows commenting' do
- should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'allows commenting' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]').find(:xpath, '..'))
end
end
context 'with an unchanged line' do
- it 'allows commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'allows commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]').find(:xpath, '..'))
end
end
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index 3bd9f5e2298..fa819cbc385 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -24,10 +24,9 @@ describe 'Merge request > User posts notes', :js do
describe 'the note form' do
it 'is valid' do
is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
- expect(find('.js-main-target-form .js-comment-button').value)
- .to eq('Comment')
+ expect(find('.js-main-target-form')).to have_selector('button', text: 'Comment')
page.within('.js-main-target-form') do
- expect(page).not_to have_link('Cancel')
+ expect(page).not_to have_button('Cancel')
end
end
@@ -60,8 +59,9 @@ describe 'Merge request > User posts notes', :js do
is_expected.to have_content('This is awesome!')
page.within('.js-main-target-form') do
expect(page).to have_no_field('note[note]', with: 'This is awesome!')
- expect(page).to have_css('.js-md-preview', visible: :hidden)
+ expect(page).to have_css('.js-vue-md-preview', visible: :hidden)
end
+ wait_for_requests
page.within('.js-main-target-form') do
is_expected.to have_css('.js-note-text', visible: true)
end
@@ -76,6 +76,7 @@ describe 'Merge request > User posts notes', :js do
end
it 'hides the toolbar buttons when previewing a note' do
+ wait_for_requests
find('.js-md-preview-button').click
page.within('.js-main-target-form') do
expect(page).not_to have_css('.md-header-toolbar.active')
@@ -84,11 +85,6 @@ describe 'Merge request > User posts notes', :js do
end
describe 'when editing a note' do
- it 'there should be a hidden edit form' do
- is_expected.to have_css('.note-edit-form:not(.mr-note-edit-form)', visible: false, count: 1)
- is_expected.to have_css('.note-edit-form.mr-note-edit-form', visible: false, count: 1)
- end
-
describe 'editing the note' do
before do
find('.note').hover
@@ -108,8 +104,8 @@ describe 'Merge request > User posts notes', :js do
within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
find('.btn-cancel').click
- expect(find('.js-note-text', visible: false).text).to eq ''
end
+ expect(find('.js-note-text').text).to eq ''
end
it 'allows using markdown buttons after saving a note and then trying to edit it again' do
@@ -118,8 +114,8 @@ describe 'Merge request > User posts notes', :js do
find('.btn-save').click
end
- wait_for_requests
find('.note').hover
+ wait_for_requests
find('.js-note-edit').click
@@ -151,13 +147,15 @@ describe 'Merge request > User posts notes', :js do
find('.js-note-edit').click
end
- it 'shows the delete link' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'shows the delete link' do
page.within('.note-attachment') do
is_expected.to have_css('.js-note-attachment-delete')
end
end
- it 'removes the attachment div and resets the edit form' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'removes the attachment div and resets the edit form' do
accept_confirm { find('.js-note-attachment-delete').click }
is_expected.not_to have_css('.note-attachment')
is_expected.not_to have_css('.current-note-edit-form')
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index 59aa90fc86f..629052442b4 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -44,7 +44,9 @@ describe 'Merge request > User resolves conflicts', :js do
within find('.diff-file', text: 'files/ruby/regex.rb') do
expect(page).to have_selector('.line_content.new', text: "def username_regexp")
+ expect(page).not_to have_selector('.line_content.new', text: "def username_regex")
expect(page).to have_selector('.line_content.new', text: "def project_name_regexp")
+ expect(page).not_to have_selector('.line_content.new', text: "def project_name_regex")
expect(page).to have_selector('.line_content.new', text: "def path_regexp")
expect(page).to have_selector('.line_content.new', text: "def archive_formats_regexp")
expect(page).to have_selector('.line_content.new', text: "def git_reference_regexp")
@@ -108,8 +110,12 @@ describe 'Merge request > User resolves conflicts', :js do
click_link('conflicts', href: %r{/conflicts\Z})
end
- include_examples "conflicts are resolved in Interactive mode"
- include_examples "conflicts are resolved in Edit inline mode"
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ # include_examples "conflicts are resolved in Interactive mode"
+ # include_examples "conflicts are resolved in Edit inline mode"
+
+ it 'prevents RSpec/EmptyExampleGroup' do
+ end
end
context 'in Parallel view mode' do
@@ -118,8 +124,12 @@ describe 'Merge request > User resolves conflicts', :js do
click_button 'Side-by-side'
end
- include_examples "conflicts are resolved in Interactive mode"
- include_examples "conflicts are resolved in Edit inline mode"
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ # include_examples "conflicts are resolved in Interactive mode"
+ # include_examples "conflicts are resolved in Edit inline mode"
+
+ it 'prevents RSpec/EmptyExampleGroup' do
+ end
end
end
@@ -138,7 +148,8 @@ describe 'Merge request > User resolves conflicts', :js do
end
end
- it 'conflicts are resolved in Edit inline mode' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'conflicts are resolved in Edit inline mode' do
within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do
wait_for_requests
find('.files-wrapper .diff-file pre')
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index 0fd2840c426..a0b9d6cb059 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -102,7 +102,8 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
describe 'timeline view' do
it 'hides when resolve discussion is clicked' do
- expect(page).to have_selector('.discussion-body', visible: false)
+ expect(page).to have_selector('.discussion-header')
+ expect(page).not_to have_selector('.discussion-body')
end
it 'shows resolved discussion when toggled' do
@@ -129,7 +130,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'hides when resolve discussion is clicked' do
- expect(page).to have_selector('.diffs .diff-file .notes_holder', visible: false)
+ expect(page).not_to have_selector('.diffs .diff-file .notes_holder')
end
it 'shows resolved discussion when toggled' do
@@ -218,10 +219,13 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
it 'updates updated text after resolving note' do
page.within '.diff-content .note' do
- find('.line-resolve-btn').click
- end
+ resolve_button = find('.line-resolve-btn')
+
+ resolve_button.click
+ wait_for_requests
- expect(page).to have_content("Resolved by #{user.name}")
+ expect(resolve_button['data-original-title']).to eq("Resolved by #{user.name}")
+ end
end
it 'hides jump to next discussion button' do
@@ -254,11 +258,16 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'resolves discussion' do
- page.all('.note .line-resolve-btn').each do |button|
+ resolve_buttons = page.all('.note .line-resolve-btn', count: 2)
+ resolve_buttons.each do |button|
button.click
end
- expect(page).to have_content('Resolved by')
+ wait_for_requests
+
+ resolve_buttons.each do |button|
+ expect(button['data-original-title']).to eq("Resolved by #{user.name}")
+ end
page.within '.line-resolve-all-container' do
expect(page).to have_content('1/1 discussion resolved')
@@ -287,7 +296,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'allows user to mark all notes as resolved' do
- page.all('.line-resolve-btn').each do |btn|
+ page.all('.note .line-resolve-btn', count: 2).each do |btn|
btn.click
end
@@ -298,7 +307,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'allows user user to mark all discussions as resolved' do
- page.all('.discussion-reply-holder').each do |reply_holder|
+ page.all('.discussion-reply-holder', count: 2).each do |reply_holder|
page.within reply_holder do
click_button 'Resolve discussion'
end
@@ -311,7 +320,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'allows user to quickly scroll to next unresolved discussion' do
- page.within first('.discussion-reply-holder') do
+ page.within('.discussion-reply-holder', match: :first) do
click_button 'Resolve discussion'
end
@@ -323,19 +332,22 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'updates updated text after resolving note' do
- page.within first('.diff-content .note') do
- find('.line-resolve-btn').click
- end
+ page.within('.diff-content .note', match: :first) do
+ resolve_button = find('.line-resolve-btn')
- expect(page).to have_content("Resolved by #{user.name}")
+ resolve_button.click
+ wait_for_requests
+
+ expect(resolve_button['data-original-title']).to eq("Resolved by #{user.name}")
+ end
end
it 'shows jump to next discussion button' do
- expect(page.all('.discussion-reply-holder')).to all(have_selector('.discussion-next-btn'))
+ expect(page.all('.discussion-reply-holder', count: 2)).to all(have_selector('.discussion-next-btn'))
end
it 'displays next discussion even if hidden' do
- page.all('.note-discussion').each do |discussion|
+ page.all('.note-discussion', count: 2).each do |discussion|
page.within discussion do
click_button 'Toggle discussion'
end
diff --git a/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
index 9ba9e8b9585..fdf9a84e997 100644
--- a/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
+++ b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
@@ -63,7 +63,7 @@ feature 'Merge request > User resolves outdated diff discussions', :js do
it 'shows that as automatically resolved' do
within(".discussion[data-discussion-id='#{outdated_discussion.id}']") do
- expect(page).to have_css('.discussion-body', visible: false)
+ expect(page).not_to have_css('.discussion-body')
expect(page).to have_content('Automatically resolved')
end
end
diff --git a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
index 3b6fffb7abd..8c2599615cb 100644
--- a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
+++ b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
@@ -17,11 +17,12 @@ describe 'Merge request > User scrolls to note on load', :js do
it 'scrolls note into view' do
visit "#{project_merge_request_path(project, merge_request)}#{fragment_id}"
+ wait_for_requests
+
page_height = page.current_window.size[1]
page_scroll_y = page.evaluate_script("window.scrollY")
fragment_position_top = page.evaluate_script("Math.round($('#{fragment_id}').offset().top)")
- expect(find('.js-toggle-content').visible?).to eq true
expect(find(fragment_id).visible?).to eq true
expect(fragment_position_top).to be >= page_scroll_y
expect(fragment_position_top).to be < (page_scroll_y + page_height)
@@ -35,7 +36,7 @@ describe 'Merge request > User scrolls to note on load', :js do
page.execute_script "window.scrollTo(0,0)"
note_element = find(fragment_id)
- note_container = note_element.ancestor('.js-toggle-container')
+ note_container = note_element.ancestor('.js-discussion-container')
expect(note_element.visible?).to eq true
@@ -44,10 +45,11 @@ describe 'Merge request > User scrolls to note on load', :js do
end
end
- it 'expands collapsed notes' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'expands collapsed notes' do
visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}"
note_element = find(collapsed_fragment_id)
- note_container = note_element.ancestor('.js-toggle-container')
+ note_container = note_element.ancestor('.timeline-content')
expect(note_element.visible?).to eq true
expect(note_container.find('.line_content.noteable_line.old', match: :first).visible?).to eq true
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index 9c0a04405a6..0a8296bd722 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -35,7 +35,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
expect(page).not_to have_selector('.diff-comment-avatar-holders')
end
- it 'does not render avatars after commening on discussion tab' do
+ it 'does not render avatars after commenting on discussion tab' do
click_button 'Reply...'
page.within('.js-discussion-note-form') do
@@ -75,7 +75,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
end
end
- %w(inline parallel).each do |view|
+ %w(parallel).each do |view|
context "#{view} view" do
before do
visit diffs_project_merge_request_path(project, merge_request, view: view)
@@ -104,7 +104,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
find('.diff-notes-collapse').send_keys(:return)
end
- expect(page).to have_selector('.notes_holder', visible: false)
+ expect(page).not_to have_selector('.notes_holder')
page.within find_line(position.line_code(project.repository)) do
first('img.js-diff-comment-avatar').click
diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb
index a9063f2bcb3..d6e7ff33d5d 100644
--- a/spec/features/merge_request/user_sees_diff_spec.rb
+++ b/spec/features/merge_request/user_sees_diff_spec.rb
@@ -6,20 +6,6 @@ describe 'Merge request > User sees diff', :js do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
- context 'when visit with */* as accept header' do
- it 'renders the notes' do
- create :note_on_merge_request, project: project, noteable: merge_request, note: 'Rebasing with master'
-
- inspect_requests(inject_headers: { 'Accept' => '*/*' }) do
- visit diffs_project_merge_request_path(project, merge_request)
- end
-
- # Load notes and diff through AJAX
- expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master')
- expect(page).to have_css('.diffs.tab-pane.active')
- end
- end
-
context 'when linking to note' do
describe 'with unresolved note' do
let(:note) { create :diff_note_on_merge_request, project: project, noteable: merge_request }
@@ -51,6 +37,7 @@ describe 'Merge request > User sees diff', :js do
context 'when merge request has overflow' do
it 'displays warning' do
allow(Commit).to receive(:max_diff_options).and_return(max_files: 3)
+ allow_any_instance_of(DiffHelper).to receive(:render_overflow_warning?).and_return(true)
visit diffs_project_merge_request_path(project, merge_request)
diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb
index d6e8c8e86ba..10390bd5864 100644
--- a/spec/features/merge_request/user_sees_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Merge request > User sees discussions' do
+describe 'Merge request > User sees discussions', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -53,11 +53,13 @@ describe 'Merge request > User sees discussions' do
shared_examples 'a functional discussion' do
let(:discussion_id) { note.discussion_id(merge_request) }
- it 'is displayed' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'is displayed' do
expect(page).to have_css(".discussion[data-discussion-id='#{discussion_id}']")
end
- it 'can be replied to' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'can be replied to' do
within(".discussion[data-discussion-id='#{discussion_id}']") do
click_button 'Reply...'
fill_in 'note[note]', with: 'Test!'
diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
index d3104b448e0..0272d300e06 100644
--- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
@@ -31,7 +31,8 @@ describe 'Merge request < User sees mini pipeline graph', :js do
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
end
- it 'avoids repeated database queries' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ xit 'avoids repeated database queries' do
before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file2)
diff --git a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
index b4cda269852..d4ad0b0a377 100644
--- a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
+++ b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
@@ -25,7 +25,7 @@ describe 'Merge request > User sees notes from forked project', :js do
page.within('.discussion-notes') do
find('.btn-text-field').click
find('#note_note').send_keys('A reply comment')
- find('.comment-btn').click
+ find('.js-comment-button').click
end
wait_for_requests
diff --git a/spec/features/merge_request/user_sees_system_notes_spec.rb b/spec/features/merge_request/user_sees_system_notes_spec.rb
index a00a682757d..c6811d4161a 100644
--- a/spec/features/merge_request/user_sees_system_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_system_notes_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Merge request > User sees system notes' do
+describe 'Merge request > User sees system notes', :js do
let(:public_project) { create(:project, :public, :repository) }
let(:private_project) { create(:project, :private, :repository) }
let(:user) { private_project.creator }
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index 3a15d70979a..11e0806ba62 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -143,9 +143,9 @@ describe 'Merge request > User sees versions', :js do
end
it_behaves_like 'allows commenting',
- file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44',
- line_code: '4_4',
- comment: 'Typo, please fix.'
+ file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44',
+ line_code: '4_4',
+ comment: 'Typo, please fix.'
end
describe 'compare with same version' do
diff --git a/spec/features/merge_request/user_uses_slash_commands_spec.rb b/spec/features/merge_request/user_uses_slash_commands_spec.rb
index 7f261b580f7..83ad4b45b5a 100644
--- a/spec/features/merge_request/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_request/user_uses_slash_commands_spec.rb
@@ -22,16 +22,24 @@ describe 'Merge request > User uses quick actions', :js do
before do
project.add_master(user)
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
end
describe 'time tracking' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
it_behaves_like 'issuable time tracker'
end
describe 'toggling the WIP prefix in the title from note' do
context 'when the current user can toggle the WIP prefix' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
it 'adds the WIP: prefix to the title' do
add_note("/wip")
@@ -56,7 +64,6 @@ describe 'Merge request > User uses quick actions', :js do
context 'when the current user cannot toggle the WIP prefix' do
before do
project.add_guest(guest)
- sign_out(:user)
sign_in(guest)
visit project_merge_request_path(project, merge_request)
end
@@ -74,6 +81,11 @@ describe 'Merge request > User uses quick actions', :js do
describe 'merging the MR from the note' do
context 'when the current user can merge the MR' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
it 'merges the MR' do
add_note("/merge")
@@ -87,6 +99,8 @@ describe 'Merge request > User uses quick actions', :js do
before do
merge_request.source_branch = 'another_branch'
merge_request.save
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
end
it 'does not merge the MR' do
@@ -101,7 +115,6 @@ describe 'Merge request > User uses quick actions', :js do
context 'when the current user cannot merge the MR' do
before do
project.add_guest(guest)
- sign_out(:user)
sign_in(guest)
visit project_merge_request_path(project, merge_request)
end
@@ -117,6 +130,11 @@ describe 'Merge request > User uses quick actions', :js do
end
describe 'adding a due date from note' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
it 'does not recognize the command nor create a note' do
add_note('/due 2016-08-28')
@@ -129,7 +147,6 @@ describe 'Merge request > User uses quick actions', :js do
let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
before do
- sign_out(:user)
another_project.add_master(user)
sign_in(user)
end
@@ -161,6 +178,11 @@ describe 'Merge request > User uses quick actions', :js do
describe '/target_branch command from note' do
context 'when the current user can change target branch' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
it 'changes target branch from a note' do
add_note("message start \n/target_branch merge-test\n message end.")
@@ -184,7 +206,6 @@ describe 'Merge request > User uses quick actions', :js do
context 'when current user can not change target branch' do
before do
project.add_guest(guest)
- sign_out(:user)
sign_in(guest)
visit project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index 96f6df587e1..b3bb8c48b4a 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -14,10 +14,10 @@ feature 'Member autocomplete', :js do
shared_examples "open suggestions when typing @" do |resource_name|
before do
page.within('.new-note') do
- if resource_name == 'issue'
- find('#note-body').send_keys('@')
- else
+ if resource_name == 'commit'
find('#note_note').send_keys('@')
+ else
+ find('#note-body').send_keys('@')
end
end
end
diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb
index 215b658eb7b..95947d2f111 100644
--- a/spec/features/profiles/account_spec.rb
+++ b/spec/features/profiles/account_spec.rb
@@ -67,4 +67,6 @@ def update_username(new_username)
page.within('.modal') do
find('.js-modal-primary-action').click
end
+
+ wait_for_requests
end
diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
index 6397df086a7..53866c32c69 100644
--- a/spec/features/projects/commit/comments/user_adds_comment_spec.rb
+++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
@@ -62,7 +62,7 @@ describe "User adds a comment on a commit", :js do
click_diff_line(sample_commit.line_code)
expect(page).to have_css(".js-temp-notes-holder form.new-note")
- .and have_css(".js-close-discussion-note-form", text: "Cancel")
+ .and have_css(".js-close-discussion-note-form", text: "Discard draft")
# The `Cancel` button closes the current form. The page should not have any open forms after that.
find(".js-close-discussion-note-form").click
diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb
new file mode 100644
index 00000000000..6397a8ad845
--- /dev/null
+++ b/spec/features/projects/commit/user_comments_on_commit_spec.rb
@@ -0,0 +1,109 @@
+require "spec_helper"
+
+describe "User comments on commit", :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:comment_text) { "XML attached" }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+ end
+
+ context "when adding new comment" do
+ it "adds comment" do
+ emoji_code = ":+1:"
+
+ page.within(".js-main-target-form") do
+ expect(page).not_to have_link("Cancel")
+
+ fill_in("note[note]", with: "#{comment_text} #{emoji_code}")
+
+ # Check on `Preview` tab
+ click_link("Preview")
+
+ expect(find(".js-md-preview")).to have_content(comment_text).and have_css("gl-emoji")
+ expect(page).not_to have_css(".js-note-text")
+
+ # Check on `Write` tab
+ click_link("Write")
+
+ expect(page).to have_field("note[note]", with: "#{comment_text} #{emoji_code}")
+
+ # Submit comment from the `Preview` tab to get rid of a separate `it` block
+ # which would specially tests if everything gets cleared from the note form.
+ click_link("Preview")
+ click_button("Comment")
+ end
+
+ wait_for_requests
+
+ page.within(".note") do
+ expect(page).to have_content(comment_text).and have_css("gl-emoji")
+ end
+
+ page.within(".js-main-target-form") do
+ expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview")
+ end
+ end
+ end
+
+ context "when editing comment" do
+ before do
+ add_note(comment_text)
+ end
+
+ it "edits comment" do
+ new_comment_text = "+1 Awesome!"
+
+ page.within(".main-notes-list") do
+ note = find(".note")
+ note.hover
+
+ note.find(".js-note-edit").click
+ end
+
+ page.find(".current-note-edit-form textarea")
+
+ page.within(".current-note-edit-form") do
+ fill_in("note[note]", with: new_comment_text)
+ click_button("Save comment")
+ end
+
+ wait_for_requests
+
+ page.within(".note") do
+ expect(page).to have_content(new_comment_text)
+ end
+ end
+ end
+
+ context "when deleting comment" do
+ before do
+ add_note(comment_text)
+ end
+
+ it "deletes comment" do
+ page.within(".note") do
+ expect(page).to have_content(comment_text)
+ end
+
+ page.within(".main-notes-list") do
+ note = find(".note")
+ note.hover
+
+ find(".more-actions").click
+ find(".more-actions .dropdown-menu li", match: :first)
+
+ accept_confirm { find(".js-note-delete").click }
+ end
+
+ expect(page).not_to have_css(".note")
+ end
+ end
+end
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 57172610aed..335174b7729 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe 'Project Graph', :js do
let(:user) { create :user }
let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let(:branch_name) { 'master' }
before do
project.add_master(user)
@@ -12,7 +13,7 @@ describe 'Project Graph', :js do
shared_examples 'page should have commits graphs' do
it 'renders commits' do
- expect(page).to have_content('Commit statistics for master')
+ expect(page).to have_content("Commit statistics for #{branch_name}")
expect(page).to have_content('Commits per day of month')
end
end
@@ -57,6 +58,23 @@ describe 'Project Graph', :js do
it_behaves_like 'page should have languages graphs'
end
+ context 'chart graph with HTML escaped branch name' do
+ let(:branch_name) { '<h1>evil</h1>' }
+
+ before do
+ project.repository.create_branch(branch_name, 'master')
+
+ visit charts_project_graph_path(project, branch_name)
+ end
+
+ it_behaves_like 'page should have commits graphs'
+
+ it 'HTML escapes branch name' do
+ expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>")
+ expect(page.body).not_to include(branch_name)
+ end
+ end
+
context 'when CI enabled' do
before do
project.enable_ci
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
index e3f90a78cb5..1828b60fec7 100644
--- a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
+++ b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
@@ -91,7 +91,7 @@ describe 'User comments on a diff', :js do
# Check the same comments in the side-by-side view.
execute_script("window.scrollTo(0,0);")
- click_link('Side-by-side')
+ click_button 'Side-by-side'
wait_for_requests
@@ -120,7 +120,7 @@ describe 'User comments on a diff', :js do
click_button('Comment')
end
- page.within('.diff-file:nth-of-type(5) .note') do
+ page.within('.diff-file:nth-of-type(5) .discussion .note') do
find('.js-note-edit').click
page.within('.current-note-edit-form') do
@@ -131,7 +131,7 @@ describe 'User comments on a diff', :js do
expect(page).not_to have_button('Save comment', disabled: true)
end
- page.within('.diff-file:nth-of-type(5) .note') do
+ page.within('.diff-file:nth-of-type(5) .discussion .note') do
expect(page).to have_content('Typo, please fix').and have_no_content('Line is wrong')
end
end
@@ -150,7 +150,7 @@ describe 'User comments on a diff', :js do
expect(page).to have_content('1')
end
- page.within('.diff-file:nth-of-type(5) .note') do
+ page.within('.diff-file:nth-of-type(5) .discussion .note') do
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
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
index 2eb652147ce..f90aaba3caf 100644
--- 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
@@ -16,7 +16,7 @@ describe 'User comments on a merge request', :js do
it 'adds a comment' do
page.within('.js-main-target-form') do
- fill_in(:note_note, with: '# Comment with a header')
+ fill_in('note[note]', with: '# Comment with a header')
click_button('Comment')
end
@@ -32,7 +32,6 @@ describe 'User comments on a merge request', :js 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
diff --git a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb
index f3e97bc9eb2..67b6aefb2d8 100644
--- a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb
@@ -13,6 +13,8 @@ describe 'User reverts a merge request', :js do
click_button('Merge')
+ wait_for_requests
+
visit(merge_request_path(merge_request))
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
index d36aafdbc54..b1bfe9e5de3 100644
--- a/spec/features/projects/merge_requests/user_views_diffs_spec.rb
+++ b/spec/features/projects/merge_requests/user_views_diffs_spec.rb
@@ -16,7 +16,7 @@ describe 'User views diffs', :js do
it 'unfolds diffs' do
first('.js-unfold').click
- expect(first('.text-file')).to have_content('.bundle')
+ expect(find('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"] .text-file')).to have_content('.bundle')
end
end
@@ -36,7 +36,7 @@ describe 'User views diffs', :js do
context 'when in the side-by-side view' do
before do
- click_link('Side-by-side')
+ click_button 'Side-by-side'
wait_for_requests
end
@@ -45,6 +45,14 @@ describe 'User views diffs', :js do
expect(page).to have_css('.parallel')
end
+ it 'toggles container class' do
+ expect(page).not_to have_css('.content-wrapper > .container-fluid.container-limited')
+
+ click_link 'Commits'
+
+ expect(page).to have_css('.content-wrapper > .container-fluid.container-limited')
+ end
+
include_examples 'unfold diffs'
end
end
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index 7f547a4ca1f..84ec32b3fac 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -59,7 +59,9 @@ describe 'View on environment', :js do
it 'has a "View on env" button' do
within '.diffs' do
- expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ text = 'View on feature.review.example.com'
+ url = 'http://feature.review.example.com/ruby/feature'
+ expect(page).to have_selector("a[data-original-title='#{text}'][href='#{url}']")
end
end
end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 1f8d31a5c88..24a2c89f50b 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -177,14 +177,35 @@ feature 'Login' do
end
context 'logging in via OAuth' do
- it 'shows 2FA prompt after OAuth login' do
- stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')
- gitlab_sign_in_via('saml', user, 'my-uid')
+ let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')}
+ let(:mock_saml_response) do
+ File.read('spec/fixtures/authentication/saml_response.xml')
+ end
- expect(page).to have_content('Two-Factor Authentication')
- enter_code(user.current_otp)
- expect(current_path).to eq root_path
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
+ providers: [mock_saml_config_with_upstream_two_factor_authn_contexts])
+ gitlab_sign_in_via('saml', user, 'my-uid', mock_saml_response)
+ end
+
+ context 'when authn_context is worth two factors' do
+ let(:mock_saml_response) do
+ File.read('spec/fixtures/authentication/saml_response.xml')
+ .gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
+ end
+
+ it 'signs user in without prompting for second factor' do
+ expect(page).not_to have_content('Two-Factor Authentication')
+ expect(current_path).to eq root_path
+ end
+ end
+
+ context 'when authn_context is not worth two factors' do
+ it 'shows 2FA prompt after OAuth login' do
+ expect(page).to have_content('Two-Factor Authentication')
+ enter_code(user.current_otp)
+ expect(current_path).to eq root_path
+ end
end
end
end
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index b51ca5d130b..bfe11ddf673 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -40,6 +40,15 @@ describe 'Signup' do
expect(find('.username')).to have_css '.gl-field-error-outline'
end
+
+ it 'shows an error message on submit if the username contains special characters' do
+ fill_in 'new_user_username', with: 'new$user!username'
+ wait_for_requests
+
+ click_button "Register"
+
+ expect(page).to have_content("Please create a username with only alphanumeric characters.")
+ end
end
context 'with no errors' do
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index f1ae2c7ab65..232f35c86f9 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -133,7 +133,7 @@ describe NotesFinder do
it 'raises an exception for an invalid target_type' do
params[:target_type] = 'invalid'
- expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')
+ expect { described_class.new(project, user, params).execute }.to raise_error("invalid target_type '#{params[:target_type]}'")
end
it 'filters out old notes' do
diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb
index 3ca0f7c3c89..da043f94021 100644
--- a/spec/finders/user_recent_events_finder_spec.rb
+++ b/spec/finders/user_recent_events_finder_spec.rb
@@ -1,31 +1,50 @@
require 'spec_helper'
describe UserRecentEventsFinder do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:project_owner) { project.creator }
- let!(:event) { create(:event, project: project, author: project_owner) }
+ let(:current_user) { create(:user) }
+ let(:project_owner) { create(:user) }
+ let(:private_project) { create(:project, :private, creator: project_owner) }
+ let(:internal_project) { create(:project, :internal, creator: project_owner) }
+ let(:public_project) { create(:project, :public, creator: project_owner) }
+ let!(:private_event) { create(:event, project: private_project, author: project_owner) }
+ let!(:internal_event) { create(:event, project: internal_project, author: project_owner) }
+ let!(:public_event) { create(:event, project: public_project, author: project_owner) }
- subject(:finder) { described_class.new(user, project_owner) }
+ subject(:finder) { described_class.new(current_user, project_owner) }
describe '#execute' do
- it 'does not include the event when a user does not have access to the project' do
- expect(finder.execute).to be_empty
+ context 'current user does not have access to projects' do
+ it 'returns public and internal events' do
+ records = finder.execute
+
+ expect(records).to include(public_event, internal_event)
+ expect(records).not_to include(private_event)
+ end
end
- context 'when the user has access to a project' do
+ context 'when current user has access to the projects' do
before do
- project.add_developer(user)
+ private_project.add_developer(current_user)
+ internal_project.add_developer(current_user)
+ public_project.add_developer(current_user)
end
- it 'includes the event' do
- expect(finder.execute).to include(event)
+ it 'returns all the events' do
+ expect(finder.execute).to include(private_event, internal_event, public_event)
end
- it 'does not include the event if the user cannot read cross project' do
- expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
+ it 'does not include the events if the user cannot read cross project' do
+ expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false }
expect(finder.execute).to be_empty
end
end
+
+ context 'when current user is anonymous' do
+ let(:current_user) { nil }
+
+ it 'returns public events only' do
+ expect(finder.execute).to eq([public_event])
+ end
+ end
end
end
diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json
index ee5588fa6c6..38ce92a5dc7 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json
@@ -109,6 +109,7 @@
"ff_only_enabled": { "type": ["boolean", false] },
"should_be_rebased": { "type": "boolean" },
"create_note_path": { "type": ["string", "null"] },
+ "preview_note_path": { "type": ["string", "null"] },
"rebase_commit_sha": { "type": ["string", "null"] },
"rebase_in_progress": { "type": "boolean" },
"can_push_to_source_branch": { "type": "boolean" },
diff --git a/spec/fixtures/authentication/saml_response.xml b/spec/fixtures/authentication/saml_response.xml
new file mode 100644
index 00000000000..ac7b662be22
--- /dev/null
+++ b/spec/fixtures/authentication/saml_response.xml
@@ -0,0 +1,42 @@
+<?xml version='1.0'?>
+<samlp:Response xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol' xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion' ID='pfxb9b71715-2202-9a51-8ae5-689d5b9dd25a' Version='2.0' IssueInstant='2014-07-17T01:01:48Z' Destination='http://sp.example.com/demo1/index.php?acs' InResponseTo='ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685'>
+ <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds='http://www.w3.org/2000/09/xmldsig#'>
+ <ds:SignedInfo><ds:CanonicalizationMethod Algorithm='http://www.w3.org/2001/10/xml-exc-c14n#'/>
+ <ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>
+ <ds:Reference URI='#pfxb9b71715-2202-9a51-8ae5-689d5b9dd25a'><ds:Transforms><ds:Transform Algorithm='http://www.w3.org/2000/09/xmldsig#enveloped-signature'/><ds:Transform Algorithm='http://www.w3.org/2001/10/xml-exc-c14n#'/></ds:Transforms><ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/><ds:DigestValue>z0Y25hsUHVJJnYhgB5LzPVjqbgM=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>NSdsZopzNX4kJETipLNbU+7dG4GPTj5e40iSBaUeUMc1UUSX4UCe9Qx6R9ADEkEQgNekgYaCFOuY90kLNh9Ky0Czq8gd4w7ykQJEVJ7VF7LakmG8dPedHAKyAMAuZ8y3mNGye31vtR9frYaznCVoxB3eAi9rbVOXkQtdOTRMHec=</ds:SignatureValue>
+ <ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
+ <samlp:Status>
+ <samlp:StatusCode Value='urn:oasis:names:tc:SAML:2.0:status:Success'/>
+ </samlp:Status>
+ <saml:Assertion xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xs='http://www.w3.org/2001/XMLSchema' ID='_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75' Version='2.0' IssueInstant='2014-07-17T01:01:48Z'>
+ <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
+ <saml:Subject>
+ <saml:NameID SPNameQualifier='http://sp.example.com/demo1/metadata.php' Format='urn:oasis:names:tc:SAML:2.0:nameid-format:transient'>_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
+ <saml:SubjectConfirmation Method='urn:oasis:names:tc:SAML:2.0:cm:bearer'>
+ <saml:SubjectConfirmationData NotOnOrAfter='2024-01-18T06:21:48Z' Recipient='http://sp.example.com/demo1/index.php?acs' InResponseTo='ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685'/>
+ </saml:SubjectConfirmation>
+ </saml:Subject>
+ <saml:Conditions NotBefore='2014-07-17T01:01:18Z' NotOnOrAfter='2024-01-18T06:21:48Z'>
+ <saml:AudienceRestriction>
+ <saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
+ </saml:AudienceRestriction>
+ </saml:Conditions>
+ <saml:AuthnStatement AuthnInstant='2014-07-17T01:01:48Z' SessionNotOnOrAfter='2024-07-17T09:01:48Z' SessionIndex='_be9967abd904ddcae3c0eb4189adbe3f71e327cf93'>
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
+ </saml:AuthnContext>
+ </saml:AuthnStatement>
+ <saml:AttributeStatement>
+ <saml:Attribute Name='uid' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
+ <saml:AttributeValue xsi:type='xs:string'>test</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name='mail' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
+ <saml:AttributeValue xsi:type='xs:string'>test@example.com</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name='eduPersonAffiliation' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
+ <saml:AttributeValue xsi:type='xs:string'>users</saml:AttributeValue>
+ <saml:AttributeValue xsi:type='xs:string'>examplerole1</saml:AttributeValue>
+ </saml:Attribute>
+ </saml:AttributeStatement>
+ </saml:Assertion>
+</samlp:Response>
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 5cf9e9e8f12..80147b13739 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -248,7 +248,7 @@ describe ProjectsHelper do
describe '#link_to_member' do
let(:group) { build_stubbed(:group) }
let(:project) { build_stubbed(:project, group: group) }
- let(:user) { build_stubbed(:user) }
+ let(:user) { build_stubbed(:user, name: '<h1>Administrator</h1>') }
describe 'using the default options' do
it 'returns an HTML link to the user' do
@@ -256,6 +256,13 @@ describe ProjectsHelper do
expect(link).to match(%r{/#{user.username}})
end
+
+ it 'HTML escapes the name of the user' do
+ link = helper.link_to_member(project, user)
+
+ expect(link).to include(ERB::Util.html_escape(user.name))
+ expect(link).not_to include(user.name)
+ end
end
end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index ff6020c8fdd..ada26b37f4a 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, no-unused-vars, quotes, prefer-template, max-len */
+/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-unused-expressions, no-unused-vars, prefer-template, max-len */
import $ from 'jquery';
import Cookies from 'js-cookie';
@@ -21,20 +21,21 @@ import '~/lib/utils/common_utils';
return setTimeout(function() {
assertFn();
return done();
- // Maybe jasmine.clock here?
+ // Maybe jasmine.clock here?
}, 333);
};
describe('AwardsHandler', function() {
- preloadFixtures('merge_requests/diff_comment.html.raw');
+ preloadFixtures('snippets/show.html.raw');
beforeEach(function(done) {
- loadFixtures('merge_requests/diff_comment.html.raw');
- $('body').attr('data-page', 'projects:merge_requests:show');
- loadAwardsHandler(true).then((obj) => {
- awardsHandler = obj;
- spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb());
- done();
- }).catch(fail);
+ loadFixtures('snippets/show.html.raw');
+ loadAwardsHandler(true)
+ .then(obj => {
+ awardsHandler = obj;
+ spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb());
+ done();
+ })
+ .catch(fail);
let isEmojiMenuBuilt = false;
openAndWaitForEmojiMenu = function() {
@@ -42,7 +43,9 @@ import '~/lib/utils/common_utils';
if (isEmojiMenuBuilt) {
resolve();
} else {
- $('.js-add-award').eq(0).click();
+ $('.js-add-award')
+ .eq(0)
+ .click();
const $menu = $('.emoji-menu');
$menu.one('build-emoji-menu-finish', () => {
isEmojiMenuBuilt = true;
@@ -63,7 +66,9 @@ import '~/lib/utils/common_utils';
});
describe('::showEmojiMenu', function() {
it('should show emoji menu when Add emoji button clicked', function(done) {
- $('.js-add-award').eq(0).click();
+ $('.js-add-award')
+ .eq(0)
+ .click();
return lazyAssert(done, function() {
var $emojiMenu;
$emojiMenu = $('.emoji-menu');
@@ -81,7 +86,9 @@ import '~/lib/utils/common_utils';
});
});
it('should remove emoji menu when body is clicked', function(done) {
- $('.js-add-award').eq(0).click();
+ $('.js-add-award')
+ .eq(0)
+ .click();
return lazyAssert(done, function() {
var $emojiMenu;
$emojiMenu = $('.emoji-menu');
@@ -92,7 +99,9 @@ import '~/lib/utils/common_utils';
});
});
it('should not remove emoji menu when search is clicked', function(done) {
- $('.js-add-award').eq(0).click();
+ $('.js-add-award')
+ .eq(0)
+ .click();
return lazyAssert(done, function() {
var $emojiMenu;
$emojiMenu = $('.emoji-menu');
@@ -103,6 +112,7 @@ import '~/lib/utils/common_utils';
});
});
});
+
describe('::addAwardToEmojiBar', function() {
it('should add emoji to votes block', function() {
var $emojiButton, $votesBlock;
@@ -139,7 +149,9 @@ import '~/lib/utils/common_utils';
$thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.userAuthored($thumbsUpEmoji);
- return expect($thumbsUpEmoji.data("originalTitle")).toBe("You cannot vote on your own issue, MR and note");
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe(
+ 'You cannot vote on your own issue, MR and note',
+ );
});
it('should restore tooltip back to initial vote list', function() {
var $thumbsUpEmoji, $votesBlock;
@@ -150,12 +162,14 @@ import '~/lib/utils/common_utils';
awardsHandler.userAuthored($thumbsUpEmoji);
jasmine.clock().tick(2801);
jasmine.clock().uninstall();
- return expect($thumbsUpEmoji.data("originalTitle")).toBe("sam");
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam');
});
});
describe('::getAwardUrl', function() {
return it('returns the url for request', function() {
- return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1/toggle_award_emoji');
+ return expect(awardsHandler.getAwardUrl()).toBe(
+ 'http://test.host/snippets/1/toggle_award_emoji',
+ );
});
});
describe('::addAward and ::checkMutuality', function() {
@@ -195,7 +209,7 @@ import '~/lib/utils/common_utils';
$thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
- return expect($thumbsUpEmoji.data("originalTitle")).toBe('You, sam, jerry, max, and andy');
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe('You, sam, jerry, max, and andy');
});
return it('handles the special case where "You" is not cleanly comma seperated', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
@@ -205,7 +219,7 @@ import '~/lib/utils/common_utils';
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
- return expect($thumbsUpEmoji.data("originalTitle")).toBe('You and sam');
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe('You and sam');
});
});
describe('::removeYouToUserList', function() {
@@ -218,7 +232,7 @@ import '~/lib/utils/common_utils';
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
- return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam, jerry, max, and andy');
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam, jerry, max, and andy');
});
return it('handles the special case where "You" is not cleanly comma seperated', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
@@ -229,7 +243,7 @@ import '~/lib/utils/common_utils';
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
- return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam');
+ return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam');
});
});
describe('::searchEmojis', () => {
@@ -245,7 +259,7 @@ import '~/lib/utils/common_utils';
expect($('.js-emoji-menu-search').val()).toBe('ali');
})
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
@@ -263,7 +277,7 @@ import '~/lib/utils/common_utils';
expect($('.js-emoji-menu-search').val()).toBe('');
})
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
@@ -272,37 +286,40 @@ import '~/lib/utils/common_utils';
describe('emoji menu', function() {
const emojiSelector = '[data-name="sunglasses"]';
const openEmojiMenuAndAddEmoji = function() {
- return openAndWaitForEmojiMenu()
- .then(() => {
- const $menu = $('.emoji-menu');
- const $block = $('.js-awards-block');
- const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector);
+ return openAndWaitForEmojiMenu().then(() => {
+ const $menu = $('.emoji-menu');
+ const $block = $('.js-awards-block');
+ const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector);
- expect($emoji.length).toBe(1);
- expect($block.find(emojiSelector).length).toBe(0);
- $emoji.click();
- expect($menu.hasClass('.is-visible')).toBe(false);
- expect($block.find(emojiSelector).length).toBe(1);
- });
+ expect($emoji.length).toBe(1);
+ expect($block.find(emojiSelector).length).toBe(0);
+ $emoji.click();
+ expect($menu.hasClass('.is-visible')).toBe(false);
+ expect($block.find(emojiSelector).length).toBe(1);
+ });
};
it('should add selected emoji to awards block', function(done) {
return openEmojiMenuAndAddEmoji()
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
it('should remove already selected emoji', function(done) {
return openEmojiMenuAndAddEmoji()
.then(() => {
- $('.js-add-award').eq(0).click();
+ $('.js-add-award')
+ .eq(0)
+ .click();
const $block = $('.js-awards-block');
- const $emoji = $('.emoji-menu').find(`.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`);
+ const $emoji = $('.emoji-menu').find(
+ `.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`,
+ );
$emoji.click();
expect($block.find(emojiSelector).length).toBe(0);
})
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
@@ -318,12 +335,12 @@ import '~/lib/utils/common_utils';
return openAndWaitForEmojiMenu()
.then(() => {
const emojiMenu = document.querySelector('.emoji-menu');
- Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), (title) => {
+ Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title => {
expect(title.textContent.trim().toLowerCase()).not.toBe('frequently used');
});
})
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
@@ -334,14 +351,15 @@ import '~/lib/utils/common_utils';
return openAndWaitForEmojiMenu()
.then(() => {
const emojiMenu = document.querySelector('.emoji-menu');
- const hasFrequentlyUsedHeading = Array.prototype.some.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title =>
- title.textContent.trim().toLowerCase() === 'frequently used'
+ const hasFrequentlyUsedHeading = Array.prototype.some.call(
+ emojiMenu.querySelectorAll('.emoji-menu-title'),
+ title => title.textContent.trim().toLowerCase() === 'frequently used',
);
expect(hasFrequentlyUsedHeading).toBe(true);
})
.then(done)
- .catch((err) => {
+ .catch(err => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
@@ -361,4 +379,4 @@ import '~/lib/utils/common_utils';
});
});
});
-}).call(window);
+}.call(window));
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index d03836d10f9..d8aa5c636da 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -4,12 +4,11 @@ import '~/behaviors/quick_submit';
describe('Quick Submit behavior', function () {
const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
- preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+ preloadFixtures('snippets/show.html.raw');
beforeEach(() => {
- loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
- $('body').attr('data-page', 'projects:merge_requests:show');
- $('form').submit((e) => {
+ loadFixtures('snippets/show.html.raw');
+ $('form').submit(e => {
// Prevent a form submit from moving us off the testing page
e.preventDefault();
});
@@ -26,24 +25,30 @@ describe('Quick Submit behavior', function () {
});
it('does not respond to other keyCodes', () => {
- this.textarea.trigger(keydownEvent({
- keyCode: 32,
- }));
+ this.textarea.trigger(
+ keydownEvent({
+ keyCode: 32,
+ }),
+ );
expect(this.spies.submit).not.toHaveBeenTriggered();
});
it('does not respond to Enter alone', () => {
- this.textarea.trigger(keydownEvent({
- ctrlKey: false,
- metaKey: false,
- }));
+ this.textarea.trigger(
+ keydownEvent({
+ ctrlKey: false,
+ metaKey: false,
+ }),
+ );
expect(this.spies.submit).not.toHaveBeenTriggered();
});
it('does not respond to repeated events', () => {
- this.textarea.trigger(keydownEvent({
- repeat: true,
- }));
+ this.textarea.trigger(
+ keydownEvent({
+ repeat: true,
+ }),
+ );
expect(this.spies.submit).not.toHaveBeenTriggered();
});
@@ -83,15 +88,21 @@ describe('Quick Submit behavior', function () {
});
it('excludes other modifier keys', () => {
- this.textarea.trigger(keydownEvent({
- altKey: true,
- }));
- this.textarea.trigger(keydownEvent({
- ctrlKey: true,
- }));
- this.textarea.trigger(keydownEvent({
- shiftKey: true,
- }));
+ this.textarea.trigger(
+ keydownEvent({
+ altKey: true,
+ }),
+ );
+ this.textarea.trigger(
+ keydownEvent({
+ ctrlKey: true,
+ }),
+ );
+ this.textarea.trigger(
+ keydownEvent({
+ shiftKey: true,
+ }),
+ );
return expect(this.spies.submit).not.toHaveBeenTriggered();
});
});
@@ -102,15 +113,21 @@ describe('Quick Submit behavior', function () {
});
it('excludes other modifier keys', () => {
- this.textarea.trigger(keydownEvent({
- altKey: true,
- }));
- this.textarea.trigger(keydownEvent({
- metaKey: true,
- }));
- this.textarea.trigger(keydownEvent({
- shiftKey: true,
- }));
+ this.textarea.trigger(
+ keydownEvent({
+ altKey: true,
+ }),
+ );
+ this.textarea.trigger(
+ keydownEvent({
+ metaKey: true,
+ }),
+ );
+ this.textarea.trigger(
+ keydownEvent({
+ shiftKey: true,
+ }),
+ );
return expect(this.spies.submit).not.toHaveBeenTriggered();
});
}
diff --git a/spec/javascripts/blob/3d_viewer/mesh_object_spec.js b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js
index d1ebae33dab..7651792be2e 100644
--- a/spec/javascripts/blob/3d_viewer/mesh_object_spec.js
+++ b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js
@@ -26,7 +26,7 @@ describe('Mesh object', () => {
const object = new MeshObject(
new BoxGeometry(10, 10, 10),
);
- const radius = object.geometry.boundingSphere.radius;
+ const { radius } = object.geometry.boundingSphere;
expect(radius).not.toBeGreaterThan(4);
});
@@ -35,7 +35,7 @@ describe('Mesh object', () => {
const object = new MeshObject(
new BoxGeometry(1, 1, 1),
);
- const radius = object.geometry.boundingSphere.radius;
+ const { radius } = object.geometry.boundingSphere;
expect(radius).toBeLessThan(1);
});
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 819ed7896ca..a18e09da50a 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -16,7 +16,7 @@ describe('Pipelines table in Commits and Merge requests', function () {
beforeEach(() => {
mock = new MockAdapter(axios);
- const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ const { pipelines } = getJSONFixture(jsonFixtureName);
PipelinesTable = Vue.extend(pipelinesTable);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js
index 4279add21d1..d1de9d132b8 100644
--- a/spec/javascripts/deploy_keys/components/key_spec.js
+++ b/spec/javascripts/deploy_keys/components/key_spec.js
@@ -88,7 +88,7 @@ describe('Deploy keys key', () => {
});
it('expands all project labels after click', done => {
- const length = vm.deployKey.deploy_keys_projects.length;
+ const { length } = vm.deployKey.deploy_keys_projects;
vm.$el.querySelectorAll('.deploy-project-label')[1].click();
Vue.nextTick(() => {
diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/app_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/changed_files_dropdown_spec.js b/spec/javascripts/diffs/components/changed_files_dropdown_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/changed_files_dropdown_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/changed_files_spec.js b/spec/javascripts/diffs/components/changed_files_spec.js
new file mode 100644
index 00000000000..2d57af6137c
--- /dev/null
+++ b/spec/javascripts/diffs/components/changed_files_spec.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import $ from 'jquery';
+import { mountComponentWithStore } from 'spec/helpers';
+import store from '~/diffs/store';
+import ChangedFiles from '~/diffs/components/changed_files.vue';
+
+describe('ChangedFiles', () => {
+ const Component = Vue.extend(ChangedFiles);
+ const createComponent = props => mountComponentWithStore(Component, { props, store });
+ let vm;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="dummy-element"></div>
+ <div class="js-tabs-affix"></div>
+ `);
+ const props = {
+ diffFiles: [
+ {
+ addedLines: 10,
+ removedLines: 20,
+ blob: {
+ path: 'some/code.txt',
+ },
+ filePath: 'some/code.txt',
+ },
+ ],
+ };
+ vm = createComponent(props);
+ });
+
+ describe('with single file added', () => {
+ it('shows files changes', () => {
+ expect(vm.$el).toContainText('1 changed file');
+ });
+
+ it('shows file additions and deletions', () => {
+ expect(vm.$el).toContainText('10 additions');
+ expect(vm.$el).toContainText('20 deletions');
+ });
+ });
+
+ describe('template', () => {
+ describe('diff view mode buttons', () => {
+ let inlineButton;
+ let parallelButton;
+
+ beforeEach(() => {
+ inlineButton = vm.$el.querySelector('.js-inline-diff-button');
+ parallelButton = vm.$el.querySelector('.js-parallel-diff-button');
+ });
+
+ it('should have Inline and Side-by-side buttons', () => {
+ expect(inlineButton).toBeDefined();
+ expect(parallelButton).toBeDefined();
+ });
+
+ it('should add active class to Inline button', done => {
+ vm.$store.state.diffs.diffViewType = 'inline';
+
+ vm.$nextTick(() => {
+ expect(inlineButton.classList.contains('active')).toEqual(true);
+ expect(parallelButton.classList.contains('active')).toEqual(false);
+
+ done();
+ });
+ });
+
+ it('should toggle active state of buttons when diff view type changed', done => {
+ vm.$store.state.diffs.diffViewType = 'parallel';
+
+ vm.$nextTick(() => {
+ expect(inlineButton.classList.contains('active')).toEqual(false);
+ expect(parallelButton.classList.contains('active')).toEqual(true);
+
+ done();
+ });
+ });
+
+ describe('clicking them', () => {
+ it('should toggle the diff view type', done => {
+ $(parallelButton).click();
+
+ vm.$nextTick(() => {
+ expect(inlineButton.classList.contains('active')).toEqual(false);
+ expect(parallelButton.classList.contains('active')).toEqual(true);
+
+ $(inlineButton).click();
+
+ vm.$nextTick(() => {
+ expect(inlineButton.classList.contains('active')).toEqual(true);
+ expect(parallelButton.classList.contains('active')).toEqual(false);
+ done();
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/compare_versions_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js
new file mode 100644
index 00000000000..dea600a783a
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_content_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+import DiffContentComponent from '~/diffs/components/diff_content.vue';
+import store from '~/mr_notes/stores';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('DiffContent', () => {
+ const Component = Vue.extend(DiffContentComponent);
+ let vm;
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+
+ beforeEach(() => {
+ vm = mountComponentWithStore(Component, {
+ store,
+ props: {
+ diffFile: getDiffFileMock(),
+ },
+ });
+ });
+
+ describe('text based files', () => {
+ it('should render diff inline view', done => {
+ vm.$store.state.diffs.diffViewType = 'inline';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(1);
+
+ done();
+ });
+ });
+
+ it('should render diff parallel view', done => {
+ vm.$store.state.diffs.diffViewType = 'parallel';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.parallel').length).toEqual(18);
+
+ done();
+ });
+ });
+ });
+
+ describe('Non-Text diffs', () => {
+ beforeEach(() => {
+ vm.diffFile.text = false;
+ });
+
+ describe('image diff', () => {
+ beforeEach(() => {
+ vm.diffFile.newPath = GREEN_BOX_IMAGE_URL;
+ vm.diffFile.newSha = 'DEF';
+ vm.diffFile.oldPath = RED_BOX_IMAGE_URL;
+ vm.diffFile.oldSha = 'ABC';
+ vm.diffFile.viewPath = '';
+ });
+
+ it('should have image diff view in place', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
+
+ expect(vm.$el.querySelectorAll('.diff-viewer .image').length).toEqual(1);
+
+ done();
+ });
+ });
+ });
+
+ describe('file diff', () => {
+ it('should have download buttons in place', done => {
+ const el = vm.$el;
+ vm.diffFile.newPath = 'test.abc';
+ vm.diffFile.newSha = 'DEF';
+ vm.diffFile.oldPath = 'test.abc';
+ vm.diffFile.oldSha = 'ABC';
+
+ vm.$nextTick(() => {
+ expect(el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
+
+ expect(el.querySelector('.deleted .file-info').textContent.trim()).toContain('test.abc');
+ expect(el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ expect(el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
+ expect(el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_discussions_spec.js b/spec/javascripts/diffs/components/diff_discussions_spec.js
new file mode 100644
index 00000000000..270f363825f
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_discussions_spec.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import discussionsMockData from '../mock_data/diff_discussions';
+
+describe('DiffDiscussions', () => {
+ let component;
+ const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+
+ beforeEach(() => {
+ component = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
+ discussions: getDiscussionsMockData(),
+ }).$mount();
+ });
+
+ describe('template', () => {
+ it('should have notes list', () => {
+ const { $el } = component;
+
+ expect($el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
new file mode 100644
index 00000000000..d0f1700bee6
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -0,0 +1,433 @@
+import Vue from 'vue';
+import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+const discussionFixture = 'merge_requests/diff_discussion.json';
+
+describe('diff_file_header', () => {
+ let vm;
+ let props;
+ const Component = Vue.extend(DiffFileHeader);
+
+ beforeEach(() => {
+ const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
+ const diffFile = convertObjectPropsToCamelCase(diffDiscussionMock.diff_file, { deep: true });
+ props = {
+ diffFile,
+ currentUser: {},
+ };
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('icon', () => {
+ beforeEach(() => {
+ props.diffFile.blob.icon = 'dummy icon';
+ });
+
+ it('returns the blob icon for files', () => {
+ props.diffFile.submodule = false;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.icon).toBe(props.diffFile.blob.icon);
+ });
+
+ it('returns the archive icon for submodules', () => {
+ props.diffFile.submodule = true;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.icon).toBe('archive');
+ });
+ });
+
+ describe('titleLink', () => {
+ beforeEach(() => {
+ Object.assign(props.diffFile, {
+ fileHash: 'badc0ffee',
+ submoduleLink: 'link://to/submodule',
+ submoduleTreeUrl: 'some://tree/url',
+ });
+ });
+
+ it('returns the fileHash for files', () => {
+ props.diffFile.submodule = false;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.titleLink).toBe(`#${props.diffFile.fileHash}`);
+ });
+
+ it('returns the submoduleTreeUrl for submodules', () => {
+ props.diffFile.submodule = true;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.titleLink).toBe(props.diffFile.submoduleTreeUrl);
+ });
+
+ it('returns the submoduleLink for submodules without submoduleTreeUrl', () => {
+ Object.assign(props.diffFile, {
+ submodule: true,
+ submoduleTreeUrl: null,
+ });
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.titleLink).toBe(props.diffFile.submoduleLink);
+ });
+ });
+
+ describe('filePath', () => {
+ beforeEach(() => {
+ Object.assign(props.diffFile, {
+ blob: { id: 'b10b1db10b1d' },
+ filePath: 'path/to/file',
+ });
+ });
+
+ it('returns the filePath for files', () => {
+ props.diffFile.submodule = false;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.filePath).toBe(props.diffFile.filePath);
+ });
+
+ it('appends the truncated blob id for submodules', () => {
+ props.diffFile.submodule = true;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.filePath).toBe(
+ `${props.diffFile.filePath} @ ${props.diffFile.blob.id.substr(0, 8)}`,
+ );
+ });
+ });
+
+ describe('titleTag', () => {
+ it('returns a link tag if fileHash is set', () => {
+ props.diffFile.fileHash = 'some hash';
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.titleTag).toBe('a');
+ });
+
+ it('returns a span tag if fileHash is not set', () => {
+ props.diffFile.fileHash = null;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.titleTag).toBe('span');
+ });
+ });
+
+ describe('isUsingLfs', () => {
+ beforeEach(() => {
+ Object.assign(props.diffFile, {
+ storedExternally: true,
+ externalStorage: 'lfs',
+ });
+ });
+
+ it('returns true if file is stored in LFS', () => {
+ vm = mountComponent(Component, props);
+
+ expect(vm.isUsingLfs).toBe(true);
+ });
+
+ it('returns false if file is not stored externally', () => {
+ props.diffFile.storedExternally = false;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.isUsingLfs).toBe(false);
+ });
+
+ it('returns false if file is not stored in LFS', () => {
+ props.diffFile.externalStorage = 'not lfs';
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.isUsingLfs).toBe(false);
+ });
+ });
+
+ describe('collapseIcon', () => {
+ it('returns chevron-down if the diff is expanded', () => {
+ props.expanded = true;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.collapseIcon).toBe('chevron-down');
+ });
+
+ it('returns chevron-right if the diff is collapsed', () => {
+ props.expanded = false;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.collapseIcon).toBe('chevron-right');
+ });
+ });
+
+ describe('isDiscussionsExpanded', () => {
+ beforeEach(() => {
+ Object.assign(props, {
+ discussionsExpanded: true,
+ expanded: true,
+ });
+ });
+
+ it('returns true if diff and discussion are expanded', () => {
+ vm = mountComponent(Component, props);
+
+ expect(vm.isDiscussionsExpanded).toBe(true);
+ });
+
+ it('returns false if discussion is collapsed', () => {
+ props.discussionsExpanded = false;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.isDiscussionsExpanded).toBe(false);
+ });
+
+ it('returns false if diff is collapsed', () => {
+ props.expanded = false;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.isDiscussionsExpanded).toBe(false);
+ });
+ });
+
+ describe('viewFileButtonText', () => {
+ it('contains the truncated content SHA', () => {
+ const dummySha = 'deebd00f is no SHA';
+ props.diffFile.contentSha = dummySha;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.viewFileButtonText).not.toContain(dummySha);
+ expect(vm.viewFileButtonText).toContain(dummySha.substr(0, 8));
+ });
+ });
+
+ describe('viewReplacedFileButtonText', () => {
+ it('contains the truncated base SHA', () => {
+ const dummySha = 'deadabba sings no more';
+ props.diffFile.diffRefs.baseSha = dummySha;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.viewReplacedFileButtonText).not.toContain(dummySha);
+ expect(vm.viewReplacedFileButtonText).toContain(dummySha.substr(0, 8));
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('handleToggle', () => {
+ beforeEach(() => {
+ spyOn(vm, '$emit').and.stub();
+ });
+
+ it('emits toggleFile if checkTarget is false', () => {
+ vm.handleToggle(null, false);
+
+ expect(vm.$emit).toHaveBeenCalledWith('toggleFile');
+ });
+
+ it('emits toggleFile if checkTarget is true and event target is header', () => {
+ vm.handleToggle({ target: vm.$refs.header }, true);
+
+ expect(vm.$emit).toHaveBeenCalledWith('toggleFile');
+ });
+
+ it('does not emit toggleFile if checkTarget is true and event target is not header', () => {
+ vm.handleToggle({ target: 'not header' }, true);
+
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('template', () => {
+ describe('collapse toggle', () => {
+ const collapseToggle = () => vm.$el.querySelector('.diff-toggle-caret');
+
+ it('is visible if collapsible is true', () => {
+ props.collapsible = true;
+
+ vm = mountComponent(Component, props);
+
+ expect(collapseToggle()).not.toBe(null);
+ });
+
+ it('is hidden if collapsible is false', () => {
+ props.collapsible = false;
+
+ vm = mountComponent(Component, props);
+
+ expect(collapseToggle()).toBe(null);
+ });
+ });
+
+ it('displays an icon in the title', () => {
+ vm = mountComponent(Component, props);
+
+ const icon = vm.$el.querySelector(`i[class="fa fa-fw fa-${vm.icon}"]`);
+ expect(icon).not.toBe(null);
+ });
+
+ describe('file paths', () => {
+ const filePaths = () => vm.$el.querySelectorAll('.file-title-name');
+
+ it('displays the path of a added file', () => {
+ props.diffFile.renamedFile = false;
+
+ vm = mountComponent(Component, props);
+
+ expect(filePaths()).toHaveLength(1);
+ expect(filePaths()[0]).toHaveText(props.diffFile.filePath);
+ });
+
+ it('displays path for deleted file', () => {
+ props.diffFile.renamedFile = false;
+ props.diffFile.deletedFile = true;
+
+ vm = mountComponent(Component, props);
+
+ expect(filePaths()).toHaveLength(1);
+ expect(filePaths()[0]).toHaveText(`${props.diffFile.filePath} deleted`);
+ });
+
+ it('displays old and new path if the file was renamed', () => {
+ props.diffFile.renamedFile = true;
+
+ vm = mountComponent(Component, props);
+
+ expect(filePaths()).toHaveLength(2);
+ expect(filePaths()[0]).toHaveText(props.diffFile.oldPath);
+ expect(filePaths()[1]).toHaveText(props.diffFile.newPath);
+ });
+ });
+
+ it('displays a copy to clipboard button', () => {
+ vm = mountComponent(Component, props);
+
+ const button = vm.$el.querySelector('.btn-clipboard');
+ expect(button).not.toBe(null);
+ expect(button.dataset.clipboardText).toBe(props.diffFile.filePath);
+ });
+
+ describe('file mode', () => {
+ it('it displays old and new file mode if it changed', () => {
+ props.diffFile.modeChanged = true;
+
+ vm = mountComponent(Component, props);
+
+ const { fileMode } = vm.$refs;
+ expect(fileMode).not.toBe(undefined);
+ expect(fileMode).toContainText(props.diffFile.aMode);
+ expect(fileMode).toContainText(props.diffFile.bMode);
+ });
+
+ it('does not display the file mode if it has not changed', () => {
+ props.diffFile.modeChanged = false;
+
+ vm = mountComponent(Component, props);
+
+ const { fileMode } = vm.$refs;
+ expect(fileMode).toBe(undefined);
+ });
+ });
+
+ describe('LFS label', () => {
+ const lfsLabel = () => vm.$el.querySelector('.label-lfs');
+
+ it('displays the LFS label for files stored in LFS', () => {
+ Object.assign(props.diffFile, {
+ storedExternally: true,
+ externalStorage: 'lfs',
+ });
+
+ vm = mountComponent(Component, props);
+
+ expect(lfsLabel()).not.toBe(null);
+ expect(lfsLabel()).toHaveText('LFS');
+ });
+
+ it('does not display the LFS label for files stored in repository', () => {
+ props.diffFile.storedExternally = false;
+
+ vm = mountComponent(Component, props);
+
+ expect(lfsLabel()).toBe(null);
+ });
+ });
+
+ describe('edit button', () => {
+ it('should not render edit button if addMergeRequestButtons is not true', () => {
+ vm = mountComponent(Component, props);
+
+ expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null);
+ });
+
+ it('should show edit button when file is editable', () => {
+ props.addMergeRequestButtons = true;
+ props.diffFile.editPath = '/';
+ vm = mountComponent(Component, props);
+
+ expect(vm.$el.querySelector('.js-edit-blob')).toContainText('Edit');
+ });
+
+ it('should not show edit button when file is deleted', () => {
+ props.addMergeRequestButtons = true;
+ props.diffFile.deletedFile = true;
+ props.diffFile.editPath = '/';
+ vm = mountComponent(Component, props);
+
+ expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null);
+ });
+ });
+
+ describe('addMergeRequestButtons', () => {
+ beforeEach(() => {
+ props.addMergeRequestButtons = true;
+ props.diffFile.editPath = '';
+ });
+
+ describe('view on environment button', () => {
+ const url = 'some.external.url/';
+ const title = 'url.title';
+
+ it('displays link to external url', () => {
+ props.diffFile.externalUrl = url;
+ props.diffFile.formattedExternalUrl = title;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.$el.querySelector(`a[href="${url}"]`)).not.toBe(null);
+ expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).not.toBe(null);
+ });
+
+ it('hides link if no external url', () => {
+ props.diffFile.externalUrl = '';
+ props.diffFile.formattedExternalUrl = title;
+
+ vm = mountComponent(Component, props);
+
+ expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).toBe(null);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
new file mode 100644
index 00000000000..1c1edfac68c
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -0,0 +1,88 @@
+import Vue from 'vue';
+import DiffFileComponent from '~/diffs/components/diff_file.vue';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('DiffFile', () => {
+ let vm;
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Vue.extend(DiffFileComponent), store, {
+ file: getDiffFileMock(),
+ currentUser: {},
+ }).$mount();
+ });
+
+ describe('template', () => {
+ it('should render component with file header, file content components', () => {
+ const el = vm.$el;
+ const { fileHash, filePath } = diffFileMockData;
+
+ expect(el.id).toEqual(fileHash);
+ expect(el.classList.contains('diff-file')).toEqual(true);
+ expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0);
+ expect(el.querySelector('.js-file-title')).toBeDefined();
+ expect(el.querySelector('.file-title-name').innerText.indexOf(filePath) > -1).toEqual(true);
+ expect(el.querySelector('.js-syntax-highlight')).toBeDefined();
+ expect(el.querySelectorAll('.line_content').length > 5).toEqual(true);
+ });
+
+ describe('collapsed', () => {
+ it('should not have file content', done => {
+ expect(vm.$el.querySelectorAll('.diff-content.hidden').length).toEqual(0);
+ expect(vm.file.collapsed).toEqual(false);
+ vm.file.collapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.diff-content.hidden').length).toEqual(1);
+
+ done();
+ });
+ });
+
+ it('should have collapsed text and link', done => {
+ vm.file.collapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.innerText).toContain('This diff is collapsed');
+ expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
+
+ done();
+ });
+ });
+
+ it('should have loading icon while loading a collapsed diffs', done => {
+ vm.file.collapsed = true;
+ vm.isLoadingCollapsedDiff = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.diff-content.loading').length).toEqual(1);
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('too large diff', () => {
+ it('should have too large warning and blob link', done => {
+ const BLOB_LINK = '/file/view/path';
+ vm.file.tooLarge = true;
+ vm.file.viewPath = BLOB_LINK;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.innerText).toContain(
+ 'This source diff could not be displayed because it is too large',
+ );
+ expect(vm.$el.querySelector('.js-too-large-diff')).toBeDefined();
+ expect(vm.$el.querySelector('.js-too-large-diff a').href.indexOf(BLOB_LINK) > -1).toEqual(
+ true,
+ );
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js
new file mode 100644
index 00000000000..0085a16815a
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js
@@ -0,0 +1,115 @@
+import Vue from 'vue';
+import DiffGutterAvatarsComponent from '~/diffs/components/diff_gutter_avatars.vue';
+import { COUNT_OF_AVATARS_IN_GUTTER } from '~/diffs/constants';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import discussionsMockData from '../mock_data/diff_discussions';
+
+describe('DiffGutterAvatars', () => {
+ let component;
+ const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+
+ beforeEach(() => {
+ component = createComponentWithStore(Vue.extend(DiffGutterAvatarsComponent), store, {
+ discussions: getDiscussionsMockData(),
+ }).$mount();
+ });
+
+ describe('computed', () => {
+ describe('discussionsExpanded', () => {
+ it('should return true when all discussions are expanded', () => {
+ expect(component.discussionsExpanded).toEqual(true);
+ });
+
+ it('should return false when all discussions are not expanded', () => {
+ component.discussions[0].expanded = false;
+ expect(component.discussionsExpanded).toEqual(false);
+ });
+ });
+
+ describe('allDiscussions', () => {
+ it('should return an array of notes', () => {
+ expect(component.allDiscussions).toEqual([...component.discussions[0].notes]);
+ });
+ });
+
+ describe('notesInGutter', () => {
+ it('should return a subset of discussions to show in gutter', () => {
+ expect(component.notesInGutter.length).toEqual(COUNT_OF_AVATARS_IN_GUTTER);
+ expect(component.notesInGutter[0]).toEqual({
+ note: component.discussions[0].notes[0].note,
+ author: component.discussions[0].notes[0].author,
+ });
+ });
+ });
+
+ describe('moreCount', () => {
+ it('should return count of remaining discussions from gutter', () => {
+ expect(component.moreCount).toEqual(2);
+ });
+ });
+
+ describe('moreText', () => {
+ it('should return proper text if moreCount > 0', () => {
+ expect(component.moreText).toEqual('2 more comments');
+ });
+
+ it('should return empty string if there is no discussion', () => {
+ component.discussions = [];
+ expect(component.moreText).toEqual('');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('getTooltipText', () => {
+ it('should return original comment if it is shorter than max length', () => {
+ const note = component.discussions[0].notes[0];
+
+ expect(component.getTooltipText(note)).toEqual('Administrator: comment 1');
+ });
+
+ it('should return truncated version of comment', () => {
+ const note = component.discussions[0].notes[1];
+
+ expect(component.getTooltipText(note)).toEqual('Fatih Acet: comment 2 is r...');
+ });
+ });
+
+ describe('toggleDiscussions', () => {
+ it('should toggle all discussions', () => {
+ expect(component.discussions[0].expanded).toEqual(true);
+
+ component.$store.dispatch('setInitialNotes', getDiscussionsMockData());
+ component.discussions = component.$store.state.notes.discussions;
+ component.toggleDiscussions();
+
+ expect(component.discussions[0].expanded).toEqual(false);
+ component.$store.dispatch('setInitialNotes', []);
+ });
+ });
+ });
+
+ describe('template', () => {
+ const buttonSelector = '.js-diff-comment-button';
+ const svgSelector = `${buttonSelector} svg`;
+ const avatarSelector = '.js-diff-comment-avatar';
+ const plusCountSelector = '.js-diff-comment-plus';
+
+ it('should have button to collapse discussions when the discussions expanded', () => {
+ expect(component.$el.querySelector(buttonSelector)).toBeDefined();
+ expect(component.$el.querySelector(svgSelector)).toBeDefined();
+ });
+
+ it('should have user avatars when discussions collapsed', () => {
+ component.discussions[0].expanded = false;
+
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector(buttonSelector)).toBeNull();
+ expect(component.$el.querySelectorAll(avatarSelector).length).toEqual(4);
+ expect(component.$el.querySelector(plusCountSelector)).toBeDefined();
+ expect(component.$el.querySelector(plusCountSelector).textContent).toEqual('+2');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
new file mode 100644
index 00000000000..cce10c4083c
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
@@ -0,0 +1,153 @@
+import Vue from 'vue';
+import DiffLineGutterContent from '~/diffs/components/diff_line_gutter_content.vue';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import {
+ MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ OLD_NO_NEW_LINE_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+} from '~/diffs/constants';
+import discussionsMockData from '../mock_data/diff_discussions';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('DiffLineGutterContent', () => {
+ const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+ const createComponent = (options = {}) => {
+ const cmp = Vue.extend(DiffLineGutterContent);
+ const props = Object.assign({}, options);
+ props.fileHash = getDiffFileMock().fileHash;
+ props.contextLinesPath = '/context/lines/path';
+
+ return createComponentWithStore(cmp, store, props).$mount();
+ };
+ const setDiscussions = component => {
+ component.$store.dispatch('setInitialNotes', getDiscussionsMockData());
+ };
+
+ const resetDiscussions = component => {
+ component.$store.dispatch('setInitialNotes', []);
+ };
+
+ describe('computed', () => {
+ describe('isMatchLine', () => {
+ it('should return true for match line type', () => {
+ const component = createComponent({ lineType: MATCH_LINE_TYPE });
+ expect(component.isMatchLine).toEqual(true);
+ });
+
+ it('should return false for non-match line type', () => {
+ const component = createComponent({ lineType: CONTEXT_LINE_TYPE });
+ expect(component.isMatchLine).toEqual(false);
+ });
+ });
+
+ describe('isContextLine', () => {
+ it('should return true for context line type', () => {
+ const component = createComponent({ lineType: CONTEXT_LINE_TYPE });
+ expect(component.isContextLine).toEqual(true);
+ });
+
+ it('should return false for non-context line type', () => {
+ const component = createComponent({ lineType: MATCH_LINE_TYPE });
+ expect(component.isContextLine).toEqual(false);
+ });
+ });
+
+ describe('isMetaLine', () => {
+ it('should return true for meta line type', () => {
+ const component = createComponent({ lineType: NEW_NO_NEW_LINE_TYPE });
+ expect(component.isMetaLine).toEqual(true);
+
+ const component2 = createComponent({ lineType: OLD_NO_NEW_LINE_TYPE });
+ expect(component2.isMetaLine).toEqual(true);
+ });
+
+ it('should return false for non-meta line type', () => {
+ const component = createComponent({ lineType: MATCH_LINE_TYPE });
+ expect(component.isMetaLine).toEqual(false);
+ });
+ });
+
+ describe('lineHref', () => {
+ it('should prepend # to lineCode', () => {
+ const lineCode = 'LC_42';
+ const component = createComponent({ lineCode });
+ expect(component.lineHref).toEqual(`#${lineCode}`);
+ });
+
+ it('should return # if there is no lineCode', () => {
+ const component = createComponent({ lineCode: null });
+ expect(component.lineHref).toEqual('#');
+ });
+ });
+
+ describe('discussions, hasDiscussions, shouldShowAvatarsOnGutter', () => {
+ it('should return empty array when there is no discussion', () => {
+ const component = createComponent({ lineCode: 'LC_42' });
+ expect(component.discussions).toEqual([]);
+ expect(component.hasDiscussions).toEqual(false);
+ expect(component.shouldShowAvatarsOnGutter).toEqual(false);
+ });
+
+ it('should return discussions for the given lineCode', () => {
+ const { lineCode } = getDiffFileMock().highlightedDiffLines[1];
+ const component = createComponent({ lineCode, showCommentButton: true });
+
+ setDiscussions(component);
+
+ expect(component.discussions).toEqual(getDiscussionsMockData());
+ expect(component.hasDiscussions).toEqual(true);
+ expect(component.shouldShowAvatarsOnGutter).toEqual(true);
+
+ resetDiscussions(component);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render three dots for context lines', () => {
+ const component = createComponent({
+ lineType: MATCH_LINE_TYPE,
+ });
+
+ expect(component.$el.querySelector('span').classList.contains('context-cell')).toEqual(true);
+ expect(component.$el.innerText).toEqual('...');
+ });
+
+ it('should render comment button', () => {
+ const component = createComponent({
+ showCommentButton: true,
+ });
+ Object.defineProperty(component, 'isLoggedIn', {
+ get() {
+ return true;
+ },
+ });
+
+ expect(component.$el.querySelector('.js-add-diff-note-button')).toBeDefined();
+ });
+
+ it('should render line link', () => {
+ const lineNumber = 42;
+ const lineCode = `LC_${lineNumber}`;
+ const component = createComponent({ lineNumber, lineCode });
+ const link = component.$el.querySelector('a');
+
+ expect(link.href.indexOf(`#${lineCode}`) > -1).toEqual(true);
+ expect(link.dataset.linenumber).toEqual(lineNumber.toString());
+ });
+
+ it('should render user avatars', () => {
+ const component = createComponent({
+ showCommentButton: true,
+ lineCode: getDiffFileMock().highlightedDiffLines[1].lineCode,
+ });
+
+ setDiscussions(component);
+ expect(component.$el.querySelector('.diff-comment-avatar-holders')).toBeDefined();
+ resetDiscussions(component);
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
new file mode 100644
index 00000000000..81cd4f9769a
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('DiffLineNoteForm', () => {
+ let component;
+ let diffFile;
+ let diffLines;
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+
+ beforeEach(() => {
+ diffFile = getDiffFileMock();
+ diffLines = diffFile.highlightedDiffLines;
+
+ component = createComponentWithStore(Vue.extend(DiffLineNoteForm), store, {
+ diffFile,
+ diffLines,
+ line: diffLines[0],
+ noteTargetLine: diffLines[0],
+ });
+
+ Object.defineProperty(component, 'isLoggedIn', {
+ get() {
+ return true;
+ },
+ });
+
+ component.$mount();
+ });
+
+ describe('methods', () => {
+ describe('handleCancelCommentForm', () => {
+ it('should call cancelCommentForm with lineCode', () => {
+ spyOn(component, 'cancelCommentForm');
+ component.handleCancelCommentForm();
+
+ expect(component.cancelCommentForm).toHaveBeenCalledWith({
+ lineCode: diffLines[0].lineCode,
+ });
+ });
+ });
+
+ describe('saveNoteForm', () => {
+ it('should call saveNote action with proper params', done => {
+ let isPromiseCalled = false;
+ const formDataSpy = spyOnDependency(DiffLineNoteForm, 'getNoteFormData').and.returnValue({
+ postData: 1,
+ });
+ const saveNoteSpy = spyOn(component, 'saveNote').and.returnValue(
+ new Promise(() => {
+ isPromiseCalled = true;
+ done();
+ }),
+ );
+
+ component.handleSaveNote('note body');
+
+ expect(formDataSpy).toHaveBeenCalled();
+ expect(saveNoteSpy).toHaveBeenCalled();
+ expect(isPromiseCalled).toEqual(true);
+ });
+ });
+ });
+
+ describe('mounted', () => {
+ it('should init autosave', () => {
+ const key = 'autosave/Note/issue///DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1';
+
+ expect(component.autosave).toBeDefined();
+ expect(component.autosave.key).toEqual(key);
+ });
+ });
+
+ describe('template', () => {
+ it('should have note form', () => {
+ const { $el } = component;
+
+ expect($el.querySelector('.js-vue-textarea')).toBeDefined();
+ expect($el.querySelector('.js-vue-issue-save')).toBeDefined();
+ expect($el.querySelector('.js-vue-markdown-field')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/edit_button_spec.js b/spec/javascripts/diffs/components/edit_button_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/edit_button_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/hidden_files_warning_spec.js b/spec/javascripts/diffs/components/hidden_files_warning_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/hidden_files_warning_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js
new file mode 100644
index 00000000000..0d5a3576204
--- /dev/null
+++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js
@@ -0,0 +1,111 @@
+import Vue from 'vue';
+import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
+import store from '~/mr_notes/stores';
+import * as constants from '~/diffs/constants';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+import discussionsMockData from '../mock_data/diff_discussions';
+
+describe('InlineDiffView', () => {
+ let component;
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+ const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+
+ beforeEach(() => {
+ const diffFile = getDiffFileMock();
+
+ component = createComponentWithStore(Vue.extend(InlineDiffView), store, {
+ diffFile,
+ diffLines: diffFile.highlightedDiffLines,
+ }).$mount();
+ });
+
+ describe('methods', () => {
+ describe('handleMouse', () => {
+ it('should set hoveredLineCode', () => {
+ expect(component.hoveredLineCode).toEqual(null);
+
+ component.handleMouse('lineCode1', true);
+ expect(component.hoveredLineCode).toEqual('lineCode1');
+
+ component.handleMouse('lineCode1', false);
+ expect(component.hoveredLineCode).toEqual(null);
+ });
+ });
+
+ describe('getLineClass', () => {
+ it('should return line class object', () => {
+ const { LINE_HOVER_CLASS_NAME, LINE_UNFOLD_CLASS_NAME } = constants;
+ const { MATCH_LINE_TYPE, NEW_LINE_TYPE } = constants;
+
+ expect(component.getLineClass(component.diffLines[0])).toEqual({
+ [NEW_LINE_TYPE]: NEW_LINE_TYPE,
+ [LINE_UNFOLD_CLASS_NAME]: false,
+ [LINE_HOVER_CLASS_NAME]: false,
+ });
+
+ component.handleMouse(component.diffLines[0].lineCode, true);
+ Object.defineProperty(component, 'isLoggedIn', {
+ get() {
+ return true;
+ },
+ });
+
+ expect(component.getLineClass(component.diffLines[0])).toEqual({
+ [NEW_LINE_TYPE]: NEW_LINE_TYPE,
+ [LINE_UNFOLD_CLASS_NAME]: false,
+ [LINE_HOVER_CLASS_NAME]: true,
+ });
+
+ expect(component.getLineClass(component.diffLines[5])).toEqual({
+ [MATCH_LINE_TYPE]: MATCH_LINE_TYPE,
+ [LINE_UNFOLD_CLASS_NAME]: true,
+ [LINE_HOVER_CLASS_NAME]: false,
+ });
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should have rendered diff lines', () => {
+ const el = component.$el;
+
+ expect(el.querySelectorAll('tr.line_holder').length).toEqual(6);
+ expect(el.querySelectorAll('tr.line_holder.new').length).toEqual(2);
+ expect(el.querySelectorAll('tr.line_holder.match').length).toEqual(1);
+ expect(el.textContent.indexOf('Bad dates') > -1).toEqual(true);
+ });
+
+ it('should render discussions', done => {
+ const el = component.$el;
+ component.$store.dispatch('setInitialNotes', getDiscussionsMockData());
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.notes_holder').length).toEqual(1);
+ expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5);
+ expect(el.innerText.indexOf('comment 5') > -1).toEqual(true);
+ component.$store.dispatch('setInitialNotes', []);
+
+ done();
+ });
+ });
+
+ it('should render new discussion forms', done => {
+ const el = component.$el;
+ const lines = getDiffFileMock().highlightedDiffLines;
+
+ component.handleShowCommentForm({ lineCode: lines[0].lineCode });
+ component.handleShowCommentForm({ lineCode: lines[1].lineCode });
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.js-vue-markdown-field').length).toEqual(2);
+ expect(el.querySelectorAll('tr')[1].classList.contains('notes_holder')).toEqual(true);
+ expect(el.querySelectorAll('tr')[3].classList.contains('notes_holder')).toEqual(true);
+
+ store.state.diffs.diffLineCommentForms = {};
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/no_changes_spec.js b/spec/javascripts/diffs/components/no_changes_spec.js
new file mode 100644
index 00000000000..7237274eb43
--- /dev/null
+++ b/spec/javascripts/diffs/components/no_changes_spec.js
@@ -0,0 +1 @@
+// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/parallel_diff_view_spec.js b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
new file mode 100644
index 00000000000..cab533217c0
--- /dev/null
+++ b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
@@ -0,0 +1,224 @@
+import Vue from 'vue';
+import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
+import store from '~/mr_notes/stores';
+import * as constants from '~/diffs/constants';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+import discussionsMockData from '../mock_data/diff_discussions';
+
+describe('ParallelDiffView', () => {
+ let component;
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+ const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+
+ beforeEach(() => {
+ const diffFile = getDiffFileMock();
+
+ component = createComponentWithStore(Vue.extend(ParallelDiffView), store, {
+ diffFile,
+ diffLines: diffFile.parallelDiffLines,
+ }).$mount();
+ });
+
+ describe('computed', () => {
+ describe('parallelDiffLines', () => {
+ it('should normalize lines for empty cells', () => {
+ expect(component.parallelDiffLines[0].left.type).toEqual(constants.EMPTY_CELL_TYPE);
+ expect(component.parallelDiffLines[1].left.type).toEqual(constants.EMPTY_CELL_TYPE);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('hasDiscussion', () => {
+ it('it should return true if there is a discussion either for left or right section', () => {
+ Object.defineProperty(component, 'discussionsByLineCode', {
+ get() {
+ return { line_42: true };
+ },
+ });
+
+ expect(component.hasDiscussion({ left: {}, right: {} })).toEqual(undefined);
+ expect(component.hasDiscussion({ left: { lineCode: 'line_42' }, right: {} })).toEqual(true);
+ expect(component.hasDiscussion({ left: {}, right: { lineCode: 'line_42' } })).toEqual(true);
+ });
+ });
+
+ describe('getClassName', () => {
+ it('should return line class object', () => {
+ const { LINE_HOVER_CLASS_NAME, LINE_UNFOLD_CLASS_NAME } = constants;
+ const { MATCH_LINE_TYPE, NEW_LINE_TYPE, LINE_POSITION_RIGHT } = constants;
+
+ expect(component.getClassName(component.diffLines[1], LINE_POSITION_RIGHT)).toEqual({
+ [NEW_LINE_TYPE]: NEW_LINE_TYPE,
+ [LINE_UNFOLD_CLASS_NAME]: false,
+ [LINE_HOVER_CLASS_NAME]: false,
+ });
+
+ const eventMock = {
+ target: component.$refs.rightLines[1],
+ };
+
+ component.handleMouse(eventMock, component.diffLines[1], true);
+ Object.defineProperty(component, 'isLoggedIn', {
+ get() {
+ return true;
+ },
+ });
+
+ expect(component.getClassName(component.diffLines[1], LINE_POSITION_RIGHT)).toEqual({
+ [NEW_LINE_TYPE]: NEW_LINE_TYPE,
+ [LINE_UNFOLD_CLASS_NAME]: false,
+ [LINE_HOVER_CLASS_NAME]: true,
+ });
+
+ expect(component.getClassName(component.diffLines[5], LINE_POSITION_RIGHT)).toEqual({
+ [MATCH_LINE_TYPE]: MATCH_LINE_TYPE,
+ [LINE_UNFOLD_CLASS_NAME]: true,
+ [LINE_HOVER_CLASS_NAME]: false,
+ });
+ });
+ });
+
+ describe('handleMouse', () => {
+ it('should set hovered line code and line section to null when isHover is false', () => {
+ const rightLineEventMock = { target: component.$refs.rightLines[1] };
+ expect(component.hoveredLineCode).toEqual(null);
+ expect(component.hoveredSection).toEqual(null);
+
+ component.handleMouse(rightLineEventMock, null, false);
+ expect(component.hoveredLineCode).toEqual(null);
+ expect(component.hoveredSection).toEqual(null);
+ });
+
+ it('should set hovered line code and line section for right section', () => {
+ const rightLineEventMock = { target: component.$refs.rightLines[1] };
+ component.handleMouse(rightLineEventMock, component.diffLines[1], true);
+ expect(component.hoveredLineCode).toEqual(component.diffLines[1].right.lineCode);
+ expect(component.hoveredSection).toEqual(constants.LINE_POSITION_RIGHT);
+ });
+
+ it('should set hovered line code and line section for left section', () => {
+ const leftLineEventMock = { target: component.$refs.leftLines[2] };
+ component.handleMouse(leftLineEventMock, component.diffLines[2], true);
+ expect(component.hoveredLineCode).toEqual(component.diffLines[2].left.lineCode);
+ expect(component.hoveredSection).toEqual(constants.LINE_POSITION_LEFT);
+ });
+ });
+
+ describe('shouldRenderDiscussions', () => {
+ it('should return true if there is a discussion on left side and it is expanded', () => {
+ const line = { left: { lineCode: 'lineCode1' } };
+ spyOn(component, 'isDiscussionExpanded').and.returnValue(true);
+ Object.defineProperty(component, 'discussionsByLineCode', {
+ get() {
+ return {
+ [line.left.lineCode]: true,
+ };
+ },
+ });
+
+ expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_LEFT)).toEqual(true);
+ expect(component.isDiscussionExpanded).toHaveBeenCalledWith(line.left.lineCode);
+ });
+
+ it('should return false if there is a discussion on left side but it is collapsed', () => {
+ const line = { left: { lineCode: 'lineCode1' } };
+ spyOn(component, 'isDiscussionExpanded').and.returnValue(false);
+ Object.defineProperty(component, 'discussionsByLineCode', {
+ get() {
+ return {
+ [line.left.lineCode]: true,
+ };
+ },
+ });
+
+ expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_LEFT)).toEqual(
+ false,
+ );
+ });
+
+ it('should return false for discussions on the right side if there is no line type', () => {
+ const CUSTOM_RIGHT_LINE_TYPE = 'CUSTOM_RIGHT_LINE_TYPE';
+ const line = { right: { lineCode: 'lineCode1', type: CUSTOM_RIGHT_LINE_TYPE } };
+ spyOn(component, 'isDiscussionExpanded').and.returnValue(true);
+ Object.defineProperty(component, 'discussionsByLineCode', {
+ get() {
+ return {
+ [line.right.lineCode]: true,
+ };
+ },
+ });
+
+ expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_RIGHT)).toEqual(
+ CUSTOM_RIGHT_LINE_TYPE,
+ );
+ });
+ });
+
+ describe('hasAnyExpandedDiscussion', () => {
+ const LINE_CODE_LEFT = 'LINE_CODE_LEFT';
+ const LINE_CODE_RIGHT = 'LINE_CODE_RIGHT';
+
+ it('should return true if there is a discussion either on the left or the right side', () => {
+ const mockLineOne = {
+ right: { lineCode: LINE_CODE_RIGHT },
+ left: {},
+ };
+ const mockLineTwo = {
+ left: { lineCode: LINE_CODE_LEFT },
+ right: {},
+ };
+
+ spyOn(component, 'isDiscussionExpanded').and.callFake(lc => lc === LINE_CODE_RIGHT);
+ expect(component.hasAnyExpandedDiscussion(mockLineOne)).toEqual(true);
+ expect(component.hasAnyExpandedDiscussion(mockLineTwo)).toEqual(false);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should have rendered diff lines', () => {
+ const el = component.$el;
+
+ expect(el.querySelectorAll('tr.line_holder.parallel').length).toEqual(6);
+ expect(el.querySelectorAll('td.empty-cell').length).toEqual(4);
+ expect(el.querySelectorAll('td.line_content.parallel.right-side').length).toEqual(6);
+ expect(el.querySelectorAll('td.line_content.parallel.left-side').length).toEqual(6);
+ expect(el.querySelectorAll('td.match').length).toEqual(4);
+ expect(el.textContent.indexOf('Bad dates') > -1).toEqual(true);
+ });
+
+ it('should render discussions', done => {
+ const el = component.$el;
+ component.$store.dispatch('setInitialNotes', getDiscussionsMockData());
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.notes_holder').length).toEqual(1);
+ expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5);
+ expect(el.innerText.indexOf('comment 5') > -1).toEqual(true);
+ component.$store.dispatch('setInitialNotes', []);
+
+ done();
+ });
+ });
+
+ it('should render new discussion forms', done => {
+ const el = component.$el;
+ const lines = getDiffFileMock().parallelDiffLines;
+
+ component.handleShowCommentForm({ lineCode: lines[0].lineCode });
+ component.handleShowCommentForm({ lineCode: lines[1].lineCode });
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.js-vue-markdown-field').length).toEqual(2);
+ expect(el.querySelectorAll('tr')[1].classList.contains('notes_holder')).toEqual(true);
+ expect(el.querySelectorAll('tr')[3].classList.contains('notes_holder')).toEqual(true);
+
+ store.state.diffs.diffLineCommentForms = {};
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js
new file mode 100644
index 00000000000..41d0dfd8939
--- /dev/null
+++ b/spec/javascripts/diffs/mock_data/diff_discussions.js
@@ -0,0 +1,496 @@
+export default {
+ id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ reply_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ position: {
+ formatter: {
+ old_line: null,
+ new_line: 2,
+ old_path: 'CHANGELOG',
+ new_path: 'CHANGELOG',
+ base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ },
+ },
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ expanded: true,
+ notes: [
+ {
+ id: 1749,
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-03T21:06:21.521Z',
+ updated_at: '2018-04-08T08:50:41.762Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 1',
+ note_html: '<p dir="auto">comment 1</p>',
+ last_edited_at: '2018-04-08T08:50:41.762Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1749/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1749&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1749',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1749',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: 1753,
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Fatih Acet',
+ username: 'fatihacet',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/fatihacevt',
+ },
+ created_at: '2018-04-08T08:49:35.804Z',
+ updated_at: '2018-04-08T08:50:45.915Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 2 is really long one',
+ note_html: '<p dir="auto">comment 2 is really long one</p>',
+ last_edited_at: '2018-04-08T08:50:45.915Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1753/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1753&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1753',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1753',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: 1754,
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:48.294Z',
+ updated_at: '2018-04-08T08:50:48.294Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 3',
+ note_html: '<p dir="auto">comment 3</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1754/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1754&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1754',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1754',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: 1755,
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:50.911Z',
+ updated_at: '2018-04-08T08:50:50.911Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 4',
+ note_html: '<p dir="auto">comment 4</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1755/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1755&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1755',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1755',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: 1756,
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:53.895Z',
+ updated_at: '2018-04-08T08:50:53.895Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 5',
+ note_html: '<p dir="auto">comment 5</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1756/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1756&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1756',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1756',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ ],
+ individual_note: false,
+ resolvable: true,
+ resolved: false,
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ diff_file: {
+ submodule: false,
+ submodule_link: null,
+ blob: {
+ id: '9e10516ca50788acf18c518a231914a21e5f16f7',
+ path: 'CHANGELOG',
+ name: 'CHANGELOG',
+ mode: '100644',
+ readable_text: true,
+ icon: 'file-text-o',
+ },
+ blob_path: 'CHANGELOG',
+ blob_name: 'CHANGELOG',
+ blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>',
+ file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
+ file_path: 'CHANGELOG',
+ new_file: false,
+ deleted_file: false,
+ renamed_file: false,
+ old_path: 'CHANGELOG',
+ new_path: 'CHANGELOG',
+ mode_changed: false,
+ a_mode: '100644',
+ b_mode: '100644',
+ text: true,
+ added_lines: 2,
+ removed_lines: 0,
+ diff_refs: {
+ base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ },
+ content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ stored_externally: null,
+ external_storage: null,
+ old_path_html: ['CHANGELOG', 'CHANGELOG'],
+ new_path_html: 'CHANGELOG',
+ context_lines_path:
+ '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff',
+ highlighted_diff_lines: [
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ old_line: null,
+ new_line: 2,
+ text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ ],
+ parallel_diff_lines: [
+ {
+ left: null,
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: null,
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ old_line: null,
+ new_line: 2,
+ text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ right: {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ },
+ ],
+ },
+ diff_discussion: true,
+ truncated_diff_lines:
+ '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new noteable_line"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new noteable_line"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n',
+ image_diff_html:
+ '<div class="image js-replaced-image" data="">\n<div class="two-up view">\n<div class="wrap">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n<div class="wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n</div>\n<div class="swipe view hide">\n<div class="swipe-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="swipe-wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n</div>\n<span class="swipe-bar">\n<span class="top-handle"></span>\n<span class="bottom-handle"></span>\n</span>\n</div>\n</div>\n<div class="onion-skin view hide">\n<div class="onion-skin-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<div class="controls">\n<div class="transparent"></div>\n<div class="drag-track">\n<div class="dragger" style="left: 0px;"></div>\n</div>\n<div class="opaque"></div>\n</div>\n</div>\n</div>\n</div>\n<div class="view-modes hide">\n<ul class="view-modes-menu">\n<li class="two-up" data-mode="two-up">2-up</li>\n<li class="swipe" data-mode="swipe">Swipe</li>\n<li class="onion-skin" data-mode="onion-skin">Onion skin</li>\n</ul>\n</div>\n',
+};
diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js
new file mode 100644
index 00000000000..d3bf9525924
--- /dev/null
+++ b/spec/javascripts/diffs/mock_data/diff_file.js
@@ -0,0 +1,220 @@
+export default {
+ submodule: false,
+ submoduleLink: null,
+ blob: {
+ id: '9e10516ca50788acf18c518a231914a21e5f16f7',
+ path: 'CHANGELOG',
+ name: 'CHANGELOG',
+ mode: '100644',
+ readableText: true,
+ icon: 'file-text-o',
+ },
+ blobPath: 'CHANGELOG',
+ blobName: 'CHANGELOG',
+ blobIcon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>',
+ fileHash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
+ filePath: 'CHANGELOG',
+ newFile: false,
+ deletedFile: false,
+ renamedFile: false,
+ oldPath: 'CHANGELOG',
+ newPath: 'CHANGELOG',
+ modeChanged: false,
+ aMode: '100644',
+ bMode: '100644',
+ text: true,
+ addedLines: 2,
+ removedLines: 0,
+ diffRefs: {
+ baseSha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ startSha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ headSha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ },
+ contentSha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ storedExternally: null,
+ externalStorage: null,
+ oldPathHtml: ['CHANGELOG', 'CHANGELOG'],
+ newPathHtml: 'CHANGELOG',
+ editPath: '/gitlab-org/gitlab-test/edit/spooky-stuff/CHANGELOG',
+ viewPath: '/gitlab-org/gitlab-test/blob/spooky-stuff/CHANGELOG',
+ replacedViewPath: null,
+ collapsed: false,
+ tooLarge: false,
+ contextLinesPath:
+ '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff',
+ highlightedDiffLines: [
+ {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ oldLine: null,
+ newLine: 1,
+ text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ richText: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ metaData: null,
+ },
+ {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ oldLine: null,
+ newLine: 2,
+ text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ richText: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ metaData: null,
+ },
+ {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ oldLine: 1,
+ newLine: 3,
+ text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ richText: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ metaData: null,
+ },
+ {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ oldLine: 2,
+ newLine: 4,
+ text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ richText: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ metaData: null,
+ },
+ {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ oldLine: 3,
+ newLine: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ richText: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ metaData: null,
+ },
+ {
+ lineCode: null,
+ type: 'match',
+ oldLine: null,
+ newLine: null,
+ text: '',
+ richText: '',
+ metaData: {
+ oldPos: 3,
+ newPos: 5,
+ },
+ },
+ ],
+ parallelDiffLines: [
+ {
+ left: {
+ type: 'empty-cell',
+ },
+ right: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ oldLine: null,
+ newLine: 1,
+ text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ richText: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ metaData: null,
+ },
+ },
+ {
+ left: {
+ type: 'empty-cell',
+ },
+ right: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ oldLine: null,
+ newLine: 2,
+ text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ richText: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ metaData: null,
+ },
+ },
+ {
+ left: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ oldLine: 1,
+ newLine: 3,
+ text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ metaData: null,
+ },
+ right: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ oldLine: 1,
+ newLine: 3,
+ text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ metaData: null,
+ },
+ },
+ {
+ left: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ oldLine: 2,
+ newLine: 4,
+ text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ richText: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ metaData: null,
+ },
+ right: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ oldLine: 2,
+ newLine: 4,
+ text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ richText: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ metaData: null,
+ },
+ },
+ {
+ left: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ oldLine: 3,
+ newLine: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ metaData: null,
+ },
+ right: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ oldLine: 3,
+ newLine: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ metaData: null,
+ },
+ },
+ {
+ left: {
+ lineCode: null,
+ type: 'match',
+ oldLine: null,
+ newLine: null,
+ text: '',
+ richText: '',
+ metaData: {
+ oldPos: 3,
+ newPos: 5,
+ },
+ },
+ right: {
+ lineCode: null,
+ type: 'match',
+ oldLine: null,
+ newLine: null,
+ text: '',
+ richText: '',
+ metaData: {
+ oldPos: 3,
+ newPos: 5,
+ },
+ },
+ },
+ ],
+};
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
new file mode 100644
index 00000000000..f0bd098f698
--- /dev/null
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -0,0 +1,211 @@
+import MockAdapter from 'axios-mock-adapter';
+import Cookies from 'js-cookie';
+import {
+ DIFF_VIEW_COOKIE_NAME,
+ INLINE_DIFF_VIEW_TYPE,
+ PARALLEL_DIFF_VIEW_TYPE,
+} from '~/diffs/constants';
+import store from '~/diffs/store';
+import * as actions from '~/diffs/store/actions';
+import * as types from '~/diffs/store/mutation_types';
+import axios from '~/lib/utils/axios_utils';
+import testAction from '../../helpers/vuex_action_helper';
+
+describe('DiffsStoreActions', () => {
+ describe('setBaseConfig', () => {
+ it('should set given endpoint and project path', done => {
+ const endpoint = '/diffs/set/endpoint';
+ const projectPath = '/root/project';
+
+ testAction(
+ actions.setBaseConfig,
+ { endpoint, projectPath },
+ { endpoint: '', projectPath: '' },
+ [{ type: types.SET_BASE_CONFIG, payload: { endpoint, projectPath } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setLoadingState', () => {
+ it('should set loading state', done => {
+ expect(store.state.diffs.isLoading).toEqual(true);
+ const loadingState = false;
+
+ testAction(
+ actions.setLoadingState,
+ loadingState,
+ {},
+ [{ type: types.SET_LOADING, payload: loadingState }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchDiffFiles', () => {
+ it('should fetch diff files', done => {
+ const endpoint = '/fetch/diff/files';
+ const mock = new MockAdapter(axios);
+ const res = { diff_files: 1, merge_request_diffs: [] };
+ mock.onGet(endpoint).reply(200, res);
+
+ testAction(
+ actions.fetchDiffFiles,
+ {},
+ { endpoint },
+ [
+ { type: types.SET_LOADING, payload: true },
+ { type: types.SET_LOADING, payload: false },
+ { type: types.SET_MERGE_REQUEST_DIFFS, payload: res.merge_request_diffs },
+ { type: types.SET_DIFF_DATA, payload: res },
+ ],
+ [],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('setInlineDiffViewType', () => {
+ it('should set diff view type to inline and also set the cookie properly', done => {
+ testAction(
+ actions.setInlineDiffViewType,
+ null,
+ {},
+ [{ type: types.SET_DIFF_VIEW_TYPE, payload: INLINE_DIFF_VIEW_TYPE }],
+ [],
+ () => {
+ setTimeout(() => {
+ expect(Cookies.get('diff_view')).toEqual(INLINE_DIFF_VIEW_TYPE);
+ done();
+ }, 0);
+ },
+ );
+ });
+ });
+
+ describe('setParallelDiffViewType', () => {
+ it('should set diff view type to parallel and also set the cookie properly', done => {
+ testAction(
+ actions.setParallelDiffViewType,
+ null,
+ {},
+ [{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }],
+ [],
+ () => {
+ setTimeout(() => {
+ expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(PARALLEL_DIFF_VIEW_TYPE);
+ done();
+ }, 0);
+ },
+ );
+ });
+ });
+
+ describe('showCommentForm', () => {
+ it('should call mutation to show comment form', done => {
+ const payload = { lineCode: 'lineCode' };
+
+ testAction(
+ actions.showCommentForm,
+ payload,
+ {},
+ [{ type: types.ADD_COMMENT_FORM_LINE, payload }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('cancelCommentForm', () => {
+ it('should call mutation to cancel comment form', done => {
+ const payload = { lineCode: 'lineCode' };
+
+ testAction(
+ actions.cancelCommentForm,
+ payload,
+ {},
+ [{ type: types.REMOVE_COMMENT_FORM_LINE, payload }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('loadMoreLines', () => {
+ it('should call mutation to show comment form', done => {
+ const endpoint = '/diffs/load/more/lines';
+ const params = { since: 6, to: 26 };
+ const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
+ const fileHash = 'ff9200';
+ const options = { endpoint, params, lineNumbers, fileHash };
+ const mock = new MockAdapter(axios);
+ const contextLines = { contextLines: [{ lineCode: 6 }] };
+ mock.onGet(endpoint).reply(200, contextLines);
+
+ testAction(
+ actions.loadMoreLines,
+ options,
+ {},
+ [
+ {
+ type: types.ADD_CONTEXT_LINES,
+ payload: { lineNumbers, contextLines, params, fileHash },
+ },
+ ],
+ [],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('loadCollapsedDiff', () => {
+ it('should fetch data and call mutation with response and the give parameter', done => {
+ const file = { hash: 123, loadCollapsedDiffUrl: '/load/collapsed/diff/url' };
+ const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] };
+ const mock = new MockAdapter(axios);
+ mock.onGet(file.loadCollapsedDiffUrl).reply(200, data);
+
+ testAction(
+ actions.loadCollapsedDiff,
+ file,
+ {},
+ [
+ {
+ type: types.ADD_COLLAPSED_DIFFS,
+ payload: { file, data },
+ },
+ ],
+ [],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('expandAllFiles', () => {
+ it('should change the collapsed prop from the diffFiles', done => {
+ testAction(
+ actions.expandAllFiles,
+ null,
+ {},
+ [
+ {
+ type: types.EXPAND_ALL_FILES,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
new file mode 100644
index 00000000000..7945ddea911
--- /dev/null
+++ b/spec/javascripts/diffs/store/getters_spec.js
@@ -0,0 +1,24 @@
+import getters from '~/diffs/store/getters';
+import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
+
+describe('DiffsStoreGetters', () => {
+ describe('isParallelView', () => {
+ it('should return true if view set to parallel view', () => {
+ expect(getters.isParallelView({ diffViewType: PARALLEL_DIFF_VIEW_TYPE })).toBeTruthy();
+ });
+
+ it('should return false if view not to parallel view', () => {
+ expect(getters.isParallelView({ diffViewType: 'foo' })).toBeFalsy();
+ });
+ });
+
+ describe('isInlineView', () => {
+ it('should return true if view set to inline view', () => {
+ expect(getters.isInlineView({ diffViewType: INLINE_DIFF_VIEW_TYPE })).toBeTruthy();
+ });
+
+ it('should return false if view not to inline view', () => {
+ expect(getters.isInlineView({ diffViewType: PARALLEL_DIFF_VIEW_TYPE })).toBeFalsy();
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
new file mode 100644
index 00000000000..02836fcaeea
--- /dev/null
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -0,0 +1,149 @@
+import mutations from '~/diffs/store/mutations';
+import * as types from '~/diffs/store/mutation_types';
+import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
+
+describe('DiffsStoreMutations', () => {
+ describe('SET_BASE_CONFIG', () => {
+ it('should set endpoint and project path', () => {
+ const state = {};
+ const endpoint = '/diffs/endpoint';
+ const projectPath = '/root/project';
+
+ mutations[types.SET_BASE_CONFIG](state, { endpoint, projectPath });
+ expect(state.endpoint).toEqual(endpoint);
+ expect(state.projectPath).toEqual(projectPath);
+ });
+ });
+
+ describe('SET_LOADING', () => {
+ it('should set loading state', () => {
+ const state = {};
+
+ mutations[types.SET_LOADING](state, false);
+ expect(state.isLoading).toEqual(false);
+ });
+ });
+
+ describe('SET_DIFF_FILES', () => {
+ it('should set diff files to state', () => {
+ const filePath = '/first-diff-file-path';
+ const state = {};
+ const diffFiles = {
+ a_mode: 1,
+ highlighted_diff_lines: [{ file_path: filePath }],
+ };
+
+ mutations[types.SET_DIFF_FILES](state, diffFiles);
+ expect(state.diffFiles.aMode).toEqual(1);
+ expect(state.diffFiles.highlightedDiffLines[0].filePath).toEqual(filePath);
+ });
+ });
+
+ describe('SET_DIFF_VIEW_TYPE', () => {
+ it('should set diff view type properly', () => {
+ const state = {};
+
+ mutations[types.SET_DIFF_VIEW_TYPE](state, INLINE_DIFF_VIEW_TYPE);
+ expect(state.diffViewType).toEqual(INLINE_DIFF_VIEW_TYPE);
+ });
+ });
+
+ describe('ADD_COMMENT_FORM_LINE', () => {
+ it('should set a truthy reference for the given line code in diffLineCommentForms', () => {
+ const state = { diffLineCommentForms: {} };
+ const lineCode = 'FDE';
+
+ mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode });
+ expect(state.diffLineCommentForms[lineCode]).toBeTruthy();
+ });
+ });
+
+ describe('REMOVE_COMMENT_FORM_LINE', () => {
+ it('should remove given reference from diffLineCommentForms', () => {
+ const state = { diffLineCommentForms: {} };
+ const lineCode = 'FDE';
+
+ mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode });
+ expect(state.diffLineCommentForms[lineCode]).toBeTruthy();
+
+ mutations[types.REMOVE_COMMENT_FORM_LINE](state, { lineCode });
+ expect(state.diffLineCommentForms[lineCode]).toBeUndefined();
+ });
+ });
+
+ describe('EXPAND_ALL_FILES', () => {
+ it('should change the collapsed prop from diffFiles', () => {
+ const diffFile = {
+ collapsed: true,
+ };
+ const state = { expandAllFiles: true, diffFiles: [diffFile] };
+
+ mutations[types.EXPAND_ALL_FILES](state);
+ expect(state.diffFiles[0].collapsed).toEqual(false);
+ });
+ });
+
+ describe('ADD_CONTEXT_LINES', () => {
+ it('should call utils.addContextLines with proper params', () => {
+ const options = {
+ lineNumbers: { oldLineNumber: 1, newLineNumber: 2 },
+ contextLines: [{ oldLine: 1 }],
+ fileHash: 'ff9200',
+ params: {
+ bottom: true,
+ },
+ };
+ const diffFile = {
+ fileHash: options.fileHash,
+ highlightedDiffLines: [],
+ parallelDiffLines: [],
+ };
+ const state = { diffFiles: [diffFile] };
+ const lines = [{ oldLine: 1 }];
+
+ const findDiffFileSpy = spyOnDependency(mutations, 'findDiffFile').and.returnValue(diffFile);
+ const removeMatchLineSpy = spyOnDependency(mutations, 'removeMatchLine');
+ const lineRefSpy = spyOnDependency(mutations, 'addLineReferences').and.returnValue(lines);
+ const addContextLinesSpy = spyOnDependency(mutations, 'addContextLines');
+
+ mutations[types.ADD_CONTEXT_LINES](state, options);
+
+ expect(findDiffFileSpy).toHaveBeenCalledWith(state.diffFiles, options.fileHash);
+ expect(removeMatchLineSpy).toHaveBeenCalledWith(
+ diffFile,
+ options.lineNumbers,
+ options.params.bottom,
+ );
+ expect(lineRefSpy).toHaveBeenCalledWith(
+ options.contextLines,
+ options.lineNumbers,
+ options.params.bottom,
+ );
+ expect(addContextLinesSpy).toHaveBeenCalledWith({
+ inlineLines: diffFile.highlightedDiffLines,
+ parallelLines: diffFile.parallelDiffLines,
+ contextLines: options.contextLines,
+ bottom: options.params.bottom,
+ lineNumbers: options.lineNumbers,
+ });
+ });
+ });
+
+ describe('ADD_COLLAPSED_DIFFS', () => {
+ it('should update the state with the given data for the given file hash', () => {
+ const spy = spyOnDependency(mutations, 'convertObjectPropsToCamelCase').and.callThrough();
+
+ const fileHash = 123;
+ const state = { diffFiles: [{}, { fileHash, existingField: 0 }] };
+ const file = { fileHash };
+ const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existingField: 1 }] };
+
+ mutations[types.ADD_COLLAPSED_DIFFS](state, { file, data });
+ expect(spy).toHaveBeenCalledWith(data, { deep: true });
+
+ expect(state.diffFiles[1].fileHash).toEqual(fileHash);
+ expect(state.diffFiles[1].existingField).toEqual(1);
+ expect(state.diffFiles[1].extraField).toEqual(1);
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
new file mode 100644
index 00000000000..5a024a0f2ad
--- /dev/null
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -0,0 +1,179 @@
+import * as utils from '~/diffs/store/utils';
+import {
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+ TEXT_DIFF_POSITION_TYPE,
+ DIFF_NOTE_TYPE,
+ NEW_LINE_TYPE,
+ OLD_LINE_TYPE,
+ MATCH_LINE_TYPE,
+ PARALLEL_DIFF_VIEW_TYPE,
+} from '~/diffs/constants';
+import { MERGE_REQUEST_NOTEABLE_TYPE } from '~/notes/constants';
+import diffFileMockData from '../mock_data/diff_file';
+import { noteableDataMock } from '../../notes/mock_data';
+
+const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+
+describe('DiffsStoreUtils', () => {
+ describe('findDiffFile', () => {
+ const files = [{ fileHash: 1, name: 'one' }];
+
+ it('should return correct file', () => {
+ expect(utils.findDiffFile(files, 1).name).toEqual('one');
+ expect(utils.findDiffFile(files, 2)).toBeUndefined();
+ });
+ });
+
+ describe('getReversePosition', () => {
+ it('should return correct line position name', () => {
+ expect(utils.getReversePosition(LINE_POSITION_RIGHT)).toEqual(LINE_POSITION_LEFT);
+ expect(utils.getReversePosition(LINE_POSITION_LEFT)).toEqual(LINE_POSITION_RIGHT);
+ });
+ });
+
+ describe('findIndexInInlineLines and findIndexInParallelLines', () => {
+ const expectSet = (method, lines, invalidLines) => {
+ expect(method(lines, { oldLineNumber: 3, newLineNumber: 5 })).toEqual(4);
+ expect(method(invalidLines || lines, { oldLineNumber: 32, newLineNumber: 53 })).toEqual(-1);
+ };
+
+ describe('findIndexInInlineLines', () => {
+ it('should return correct index for given line numbers', () => {
+ expectSet(utils.findIndexInInlineLines, getDiffFileMock().highlightedDiffLines);
+ });
+ });
+
+ describe('findIndexInParallelLines', () => {
+ it('should return correct index for given line numbers', () => {
+ expectSet(utils.findIndexInParallelLines, getDiffFileMock().parallelDiffLines, {});
+ });
+ });
+ });
+
+ describe('removeMatchLine', () => {
+ it('should remove match line properly by regarding the bottom parameter', () => {
+ const diffFile = getDiffFileMock();
+ const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
+ const inlineIndex = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
+ const parallelIndex = utils.findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
+ const atInlineIndex = diffFile.highlightedDiffLines[inlineIndex];
+ const atParallelIndex = diffFile.parallelDiffLines[parallelIndex];
+
+ utils.removeMatchLine(diffFile, lineNumbers, false);
+ expect(diffFile.highlightedDiffLines[inlineIndex]).not.toEqual(atInlineIndex);
+ expect(diffFile.parallelDiffLines[parallelIndex]).not.toEqual(atParallelIndex);
+
+ utils.removeMatchLine(diffFile, lineNumbers, true);
+ expect(diffFile.highlightedDiffLines[inlineIndex + 1]).not.toEqual(atInlineIndex);
+ expect(diffFile.parallelDiffLines[parallelIndex + 1]).not.toEqual(atParallelIndex);
+ });
+ });
+
+ describe('addContextLines', () => {
+ it('should add context lines properly with bottom parameter', () => {
+ const diffFile = getDiffFileMock();
+ const inlineLines = diffFile.highlightedDiffLines;
+ const parallelLines = diffFile.parallelDiffLines;
+ const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
+ const contextLines = [{ lineNumber: 42 }];
+ const options = { inlineLines, parallelLines, contextLines, lineNumbers, bottom: true };
+ const inlineIndex = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
+ const parallelIndex = utils.findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
+ const normalizedParallelLine = {
+ left: options.contextLines[0],
+ right: options.contextLines[0],
+ };
+
+ utils.addContextLines(options);
+ expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]);
+ expect(parallelLines[parallelLines.length - 1]).toEqual(normalizedParallelLine);
+
+ delete options.bottom;
+ utils.addContextLines(options);
+ expect(inlineLines[inlineIndex]).toEqual(contextLines[0]);
+ expect(parallelLines[parallelIndex]).toEqual(normalizedParallelLine);
+ });
+ });
+
+ describe('getNoteFormData', () => {
+ it('should properly create note form data', () => {
+ const diffFile = getDiffFileMock();
+ noteableDataMock.targetType = MERGE_REQUEST_NOTEABLE_TYPE;
+
+ const options = {
+ note: 'Hello world!',
+ noteableData: noteableDataMock,
+ noteableType: MERGE_REQUEST_NOTEABLE_TYPE,
+ diffFile,
+ noteTargetLine: {
+ lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ metaData: null,
+ newLine: 3,
+ oldLine: 1,
+ },
+ diffViewType: PARALLEL_DIFF_VIEW_TYPE,
+ linePosition: LINE_POSITION_LEFT,
+ };
+
+ const position = JSON.stringify({
+ base_sha: diffFile.diffRefs.baseSha,
+ start_sha: diffFile.diffRefs.startSha,
+ head_sha: diffFile.diffRefs.headSha,
+ old_path: diffFile.oldPath,
+ new_path: diffFile.newPath,
+ position_type: TEXT_DIFF_POSITION_TYPE,
+ old_line: options.noteTargetLine.oldLine,
+ new_line: options.noteTargetLine.newLine,
+ });
+
+ const postData = {
+ view: options.diffViewType,
+ line_type: options.linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE,
+ merge_request_diff_head_sha: diffFile.diffRefs.headSha,
+ in_reply_to_discussion_id: '',
+ note_project_id: '',
+ target_type: options.noteableType,
+ target_id: options.noteableData.id,
+ note: {
+ noteable_type: options.noteableType,
+ noteable_id: options.noteableData.id,
+ commit_id: '',
+ type: DIFF_NOTE_TYPE,
+ line_code: options.noteTargetLine.lineCode,
+ note: options.note,
+ position,
+ },
+ };
+
+ expect(utils.getNoteFormData(options)).toEqual({
+ endpoint: options.noteableData.create_note_path,
+ data: postData,
+ });
+ });
+ });
+
+ describe('addLineReferences', () => {
+ const lineNumbers = { oldLineNumber: 3, newLineNumber: 4 };
+
+ it('should add correct line references when bottom set to true', () => {
+ const lines = [{ type: null }, { type: MATCH_LINE_TYPE }];
+ const linesWithReferences = utils.addLineReferences(lines, lineNumbers, true);
+
+ expect(linesWithReferences[0].oldLine).toEqual(lineNumbers.oldLineNumber + 1);
+ expect(linesWithReferences[0].newLine).toEqual(lineNumbers.newLineNumber + 1);
+ expect(linesWithReferences[1].metaData.oldPos).toEqual(4);
+ expect(linesWithReferences[1].metaData.newPos).toEqual(5);
+ });
+
+ it('should add correct line references when bottom falsy', () => {
+ const lines = [{ type: null }, { type: MATCH_LINE_TYPE }, { type: null }];
+ const linesWithReferences = utils.addLineReferences(lines, lineNumbers);
+
+ expect(linesWithReferences[0].oldLine).toEqual(0);
+ expect(linesWithReferences[0].newLine).toEqual(1);
+ expect(linesWithReferences[1].metaData.oldPos).toEqual(2);
+ expect(linesWithReferences[1].metaData.newPos).toEqual(3);
+ });
+ });
+});
diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
index 59bd2650081..d926663fac0 100644
--- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -103,7 +103,7 @@ describe('RecentSearchesDropdownContent', () => {
describe('processedItems', () => {
it('with items', () => {
vm = createComponent(propsDataWithItems);
- const processedItems = vm.processedItems;
+ const { processedItems } = vm;
expect(processedItems.length).toEqual(2);
@@ -122,7 +122,7 @@ describe('RecentSearchesDropdownContent', () => {
it('with no items', () => {
vm = createComponent(propsDataWithoutItems);
- const processedItems = vm.processedItems;
+ const { processedItems } = vm;
expect(processedItems.length).toEqual(0);
});
@@ -131,13 +131,13 @@ describe('RecentSearchesDropdownContent', () => {
describe('hasItems', () => {
it('with items', () => {
vm = createComponent(propsDataWithItems);
- const hasItems = vm.hasItems;
+ const { hasItems } = vm;
expect(hasItems).toEqual(true);
});
it('with no items', () => {
vm = createComponent(propsDataWithoutItems);
- const hasItems = vm.hasItems;
+ const { hasItems } = vm;
expect(hasItems).toEqual(false);
});
});
diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
index fbc3926d332..68158cf52e4 100644
--- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
@@ -17,6 +17,17 @@ describe('Filtered Search Token Keys', () => {
});
});
+ describe('getKeys', () => {
+ it('should return keys', () => {
+ const getKeys = FilteredSearchTokenKeys.getKeys();
+ const keys = FilteredSearchTokenKeys.get().map(i => i.key);
+
+ keys.forEach((key, i) => {
+ expect(key).toEqual(getKeys[i]);
+ });
+ });
+ });
+
describe('getConditions', () => {
let conditions;
diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/javascripts/filtered_search/recent_searches_root_spec.js
index 1e6272bad0b..d063fcf4f2d 100644
--- a/spec/javascripts/filtered_search/recent_searches_root_spec.js
+++ b/spec/javascripts/filtered_search/recent_searches_root_spec.js
@@ -15,8 +15,7 @@ describe('RecentSearchesRoot', () => {
};
VueSpy = spyOnDependency(RecentSearchesRoot, 'Vue').and.callFake((options) => {
- data = options.data;
- template = options.template;
+ ({ data, template } = options);
});
RecentSearchesRoot.prototype.render.call(recentSearchesRoot);
diff --git a/spec/javascripts/fixtures/commit.rb b/spec/javascripts/fixtures/commit.rb
new file mode 100644
index 00000000000..351db6ba184
--- /dev/null
+++ b/spec/javascripts/fixtures/commit.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+ let(:commit) { project.commit("master") }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('commit/')
+ end
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it 'commit/show.html.raw' do |example|
+ params = {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: commit.id
+ }
+
+ get :show, params
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb
index fa97f352e31..38fc963caf7 100644
--- a/spec/javascripts/fixtures/snippet.rb
+++ b/spec/javascripts/fixtures/snippet.rb
@@ -7,6 +7,7 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let(:snippet) { create(:personal_snippet, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: admin) }
+ let!(:snippet_note) { create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') }
render_views
diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js
index 2839020b2ca..21c462cd040 100644
--- a/spec/javascripts/gl_field_errors_spec.js
+++ b/spec/javascripts/gl_field_errors_spec.js
@@ -18,7 +18,7 @@ describe('GL Style Field Errors', function() {
expect(this.$form).toBeDefined();
expect(this.$form.length).toBe(1);
expect(this.fieldErrors).toBeDefined();
- const inputs = this.fieldErrors.state.inputs;
+ const { inputs } = this.fieldErrors.state;
expect(inputs.length).toBe(4);
});
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index 2b92c485f41..03d4b472b87 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -67,7 +67,7 @@ describe('AppComponent', () => {
it('should return list of groups from store', () => {
spyOn(vm.store, 'getGroups');
- const groups = vm.groups;
+ const { groups } = vm;
expect(vm.store.getGroups).toHaveBeenCalled();
expect(groups).not.toBeDefined();
});
@@ -77,7 +77,7 @@ describe('AppComponent', () => {
it('should return pagination info from store', () => {
spyOn(vm.store, 'getPaginationInfo');
- const pageInfo = vm.pageInfo;
+ const { pageInfo } = vm;
expect(vm.store.getPaginationInfo).toHaveBeenCalled();
expect(pageInfo).not.toBeDefined();
});
@@ -293,7 +293,7 @@ describe('AppComponent', () => {
beforeEach(() => {
groupItem = Object.assign({}, mockParentGroupItem);
groupItem.children = mockChildren;
- childGroupItem = groupItem.children[0];
+ [childGroupItem] = groupItem.children;
groupItem.isChildrenLoading = false;
vm.targetGroup = childGroupItem;
vm.targetParentGroup = groupItem;
diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js
index 49a139855c8..d0cac5efc40 100644
--- a/spec/javascripts/groups/components/group_item_spec.js
+++ b/spec/javascripts/groups/components/group_item_spec.js
@@ -41,7 +41,7 @@ describe('GroupItemComponent', () => {
describe('rowClass', () => {
it('should return map of classes based on group details', () => {
const classes = ['is-open', 'has-children', 'has-description', 'being-removed'];
- const rowClass = vm.rowClass;
+ const { rowClass } = vm;
expect(Object.keys(rowClass).length).toBe(classes.length);
Object.keys(rowClass).forEach((className) => {
diff --git a/spec/javascripts/helpers/index.js b/spec/javascripts/helpers/index.js
new file mode 100644
index 00000000000..d2c5caf0bdb
--- /dev/null
+++ b/spec/javascripts/helpers/index.js
@@ -0,0 +1,3 @@
+import mountComponent, { mountComponentWithStore } from './vue_mount_component_helper';
+
+export { mountComponent, mountComponentWithStore };
diff --git a/spec/javascripts/helpers/init_vue_mr_page_helper.js b/spec/javascripts/helpers/init_vue_mr_page_helper.js
new file mode 100644
index 00000000000..05c6d587e9c
--- /dev/null
+++ b/spec/javascripts/helpers/init_vue_mr_page_helper.js
@@ -0,0 +1,42 @@
+import initMRPage from '~/mr_notes/index';
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
+import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data';
+import diffFileMockData from '../diffs/mock_data/diff_file';
+
+export default function initVueMRPage() {
+ const diffsAppEndpoint = '/diffs/app/endpoint';
+ const diffsAppProjectPath = 'testproject';
+ const mrEl = document.createElement('div');
+ mrEl.className = 'merge-request fixture-mr';
+ mrEl.setAttribute('data-mr-action', 'diffs');
+ document.body.appendChild(mrEl);
+
+ const mrDiscussionsEl = document.createElement('div');
+ mrDiscussionsEl.id = 'js-vue-mr-discussions';
+ mrDiscussionsEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
+ mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock));
+ mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock));
+ mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request');
+ document.body.appendChild(mrDiscussionsEl);
+
+ const discussionCounterEl = document.createElement('div');
+ discussionCounterEl.id = 'js-vue-discussion-counter';
+ document.body.appendChild(discussionCounterEl);
+
+ const diffsAppEl = document.createElement('div');
+ diffsAppEl.id = 'js-diffs-app';
+ diffsAppEl.setAttribute('data-endpoint', diffsAppEndpoint);
+ diffsAppEl.setAttribute('data-project-path', diffsAppProjectPath);
+ diffsAppEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
+ document.body.appendChild(diffsAppEl);
+
+ const mock = new MockAdapter(axios);
+ mock.onGet(diffsAppEndpoint).reply(200, {
+ branch_name: 'foo',
+ diff_files: [diffFileMockData],
+ });
+
+ initMRPage();
+ return mock;
+}
diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js
index 8cabc6e8935..fc0695a4263 100644
--- a/spec/javascripts/ide/components/repo_tab_spec.js
+++ b/spec/javascripts/ide/components/repo_tab_spec.js
@@ -38,6 +38,26 @@ describe('RepoTab', () => {
expect(name.textContent.trim()).toEqual(vm.tab.name);
});
+ it('does not call openPendingTab when tab is active', done => {
+ vm = createComponent({
+ tab: {
+ ...file(),
+ pending: true,
+ active: true,
+ },
+ });
+
+ spyOn(vm, 'openPendingTab');
+
+ vm.$el.click();
+
+ vm.$nextTick(() => {
+ expect(vm.openPendingTab).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
it('fires clickFile when the link is clicked', () => {
vm = createComponent({
tab: file(),
@@ -112,9 +132,9 @@ describe('RepoTab', () => {
});
it('renders a tooltip', () => {
- expect(
- vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle,
- ).toContain('Locked by testuser');
+ expect(vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle).toContain(
+ 'Locked by testuser',
+ );
});
});
diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js
index 9312e17704e..569fa5c7aae 100644
--- a/spec/javascripts/ide/helpers.js
+++ b/spec/javascripts/ide/helpers.js
@@ -1,3 +1,4 @@
+import * as pathUtils from 'path';
import { decorateData } from '~/ide/stores/utils';
import state from '~/ide/stores/state';
import commitState from '~/ide/stores/modules/commit/state';
@@ -14,13 +15,34 @@ export const resetStore = store => {
store.replaceState(newState);
};
-export const file = (name = 'name', id = name, type = '') =>
+export const file = (name = 'name', id = name, type = '', parent = null) =>
decorateData({
id,
type,
icon: 'icon',
url: 'url',
name,
- path: name,
+ path: parent ? `${parent.path}/${name}` : name,
+ parentPath: parent ? parent.path : '',
lastCommit: {},
});
+
+export const createEntriesFromPaths = paths =>
+ paths
+ .map(path => ({
+ name: pathUtils.basename(path),
+ dir: pathUtils.dirname(path),
+ ext: pathUtils.extname(path),
+ }))
+ .reduce((entries, path, idx) => {
+ const { name } = path;
+ const parent = path.dir ? entries[path.dir] : null;
+ const type = path.ext ? 'blob' : 'tree';
+
+ const entry = file(name, (idx + 1).toString(), type, parent);
+
+ return {
+ [entry.path]: entry,
+ ...entries,
+ };
+ }, {});
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
index 96abd1dcd9e..90ebb95b687 100644
--- a/spec/javascripts/ide/lib/diff/controller_spec.js
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -63,7 +63,7 @@ describe('Multi-file editor library dirty diff controller', () => {
[type]: true,
};
- const range = getDecorator(change).range;
+ const { range } = getDecorator(change);
expect(range.startLineNumber).toBe(1);
expect(range.endLineNumber).toBe(2);
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index e0ef57a3966..cefed9ddb43 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
+import testAction from 'spec/helpers/vuex_action_helper';
+import { showTreeEntry } from '~/ide/stores/actions/tree';
+import * as types from '~/ide/stores/mutation_types';
import store from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
-import { file, resetStore } from '../../helpers';
+import { file, resetStore, createEntriesFromPaths } from '../../helpers';
describe('Multi-file store tree actions', () => {
let projectTree;
@@ -96,6 +99,37 @@ describe('Multi-file store tree actions', () => {
});
});
+ describe('showTreeEntry', () => {
+ beforeEach(() => {
+ const paths = [
+ 'grandparent',
+ 'ancestor',
+ 'grandparent/parent',
+ 'grandparent/aunt',
+ 'grandparent/parent/child.txt',
+ 'grandparent/aunt/cousing.txt',
+ ];
+
+ Object.assign(store.state.entries, createEntriesFromPaths(paths));
+ });
+
+ it('opens the parents', done => {
+ testAction(
+ showTreeEntry,
+ 'grandparent/parent/child.txt',
+ store.state,
+ [
+ { type: types.SET_TREE_OPEN, payload: 'grandparent/parent' },
+ { type: types.SET_TREE_OPEN, payload: 'grandparent' },
+ ],
+ [
+ { type: 'showTreeEntry' },
+ ],
+ done,
+ );
+ });
+ });
+
describe('getLastCommitData', () => {
beforeEach(() => {
spyOn(service, 'getTreeLastCommit').and.returnValue(
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index a9ec7f42a9d..41ff59949e5 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -556,5 +556,75 @@ describe('common_utils', () => {
expect(Object.keys(commonUtils.convertObjectPropsToCamelCase()).length).toBe(0);
expect(Object.keys(commonUtils.convertObjectPropsToCamelCase({})).length).toBe(0);
});
+
+ it('does not deep-convert by default', () => {
+ const obj = {
+ snake_key: {
+ child_snake_key: 'value',
+ },
+ };
+
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(obj),
+ ).toEqual({
+ snakeKey: {
+ child_snake_key: 'value',
+ },
+ });
+ });
+
+ describe('deep: true', () => {
+ it('converts object with child objects', () => {
+ const obj = {
+ snake_key: {
+ child_snake_key: 'value',
+ },
+ };
+
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(obj, { deep: true }),
+ ).toEqual({
+ snakeKey: {
+ childSnakeKey: 'value',
+ },
+ });
+ });
+
+ it('converts array with child objects', () => {
+ const arr = [
+ {
+ child_snake_key: 'value',
+ },
+ ];
+
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }),
+ ).toEqual([
+ {
+ childSnakeKey: 'value',
+ },
+ ]);
+ });
+
+ it('converts array with child arrays', () => {
+ const arr = [
+ [
+ {
+ child_snake_key: 'value',
+ },
+ ],
+ ];
+
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }),
+ ).toEqual([
+ [
+ {
+ childSnakeKey: 'value',
+ },
+ ],
+ ]);
+ });
+ });
});
});
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index eab5c24406a..33987574f00 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -96,4 +96,20 @@ describe('text_utility', () => {
expect(textUtils.convertToSentenceCase('Hello World')).toBe('Hello world');
});
});
+
+ describe('truncateSha', () => {
+ it('shortens SHAs to 8 characters', () => {
+ expect(textUtils.truncateSha('verylongsha')).toBe('verylong');
+ });
+
+ it('leaves short SHAs as is', () => {
+ expect(textUtils.truncateSha('shortsha')).toBe('shortsha');
+ });
+ });
+
+ describe('splitCamelCase', () => {
+ it('separates a PascalCase word to two', () => {
+ expect(textUtils.splitCamelCase('HelloWorld')).toBe('Hello World');
+ });
+ });
});
diff --git a/spec/javascripts/matchers.js b/spec/javascripts/matchers.js
index 7cc5e753c22..0d465510fd3 100644
--- a/spec/javascripts/matchers.js
+++ b/spec/javascripts/matchers.js
@@ -1,4 +1,16 @@
export default {
+ toContainText: () => ({
+ compare(vm, text) {
+ if (!(vm.$el instanceof HTMLElement)) {
+ throw new Error('vm.$el is not a DOM element!');
+ }
+
+ const result = {
+ pass: vm.$el.innerText.includes(text),
+ };
+ return result;
+ },
+ }),
toHaveSpriteIcon: () => ({
compare(element, iconName) {
if (!iconName) {
@@ -10,7 +22,9 @@ export default {
}
const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
- const matchingIcon = iconReferences.find(reference => reference.getAttribute('xlink:href').endsWith(`#${iconName}`));
+ const matchingIcon = iconReferences.find(reference =>
+ reference.getAttribute('xlink:href').endsWith(`#${iconName}`),
+ );
const result = {
pass: !!matchingIcon,
};
@@ -20,7 +34,7 @@ export default {
} else {
result.message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`;
- const existingIcons = iconReferences.map((reference) => {
+ const existingIcons = iconReferences.map(reference => {
const iconUrl = reference.getAttribute('xlink:href');
return `"${iconUrl.replace(/^.+#/, '')}"`;
});
@@ -32,4 +46,12 @@ export default {
return result;
},
}),
+ toRender: () => ({
+ compare(vm) {
+ const result = {
+ pass: vm.$el.nodeType !== Node.COMMENT_NODE,
+ };
+ return result;
+ },
+ }),
};
diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js
deleted file mode 100644
index dc9dc4d4249..00000000000
--- a/spec/javascripts/merge_request_notes_spec.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import $ from 'jquery';
-import _ from 'underscore';
-import 'autosize';
-import '~/gl_form';
-import '~/lib/utils/text_utility';
-import '~/behaviors/markdown/render_gfm';
-import Notes from '~/notes';
-
-const upArrowKeyCode = 38;
-
-describe('Merge request notes', () => {
- window.gon = window.gon || {};
- window.gl = window.gl || {};
- gl.utils = gl.utils || {};
-
- const discussionTabFixture = 'merge_requests/diff_comment.html.raw';
- const changesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json';
- preloadFixtures(discussionTabFixture, changesTabJsonFixture);
-
- describe('Discussion tab with diff comments', () => {
- beforeEach(() => {
- loadFixtures(discussionTabFixture);
- gl.utils.disableButtonIfEmptyField = _.noop;
- window.project_uploads_path = 'http://test.host/uploads';
- $('body').attr('data-page', 'projects:merge_requests:show');
- window.gon.current_user_id = $('.note:last').data('authorId');
-
- return new Notes('', []);
- });
-
- afterEach(() => {
- // Undo what we did to the shared <body>
- $('body').removeAttr('data-page');
- });
-
- describe('up arrow', () => {
- it('edits last comment when triggered in main form', () => {
- const upArrowEvent = $.Event('keydown');
- upArrowEvent.which = upArrowKeyCode;
-
- spyOnEvent('.note:last .js-note-edit', 'click');
-
- $('.js-note-text').trigger(upArrowEvent);
-
- expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit');
- });
-
- it('edits last comment in discussion when triggered in discussion form', (done) => {
- const upArrowEvent = $.Event('keydown');
- upArrowEvent.which = upArrowKeyCode;
-
- spyOnEvent('.note-discussion .js-note-edit', 'click');
-
- $('.js-discussion-reply-button').click();
-
- setTimeout(() => {
- expect(
- $('.note-discussion .js-note-text'),
- ).toExist();
-
- $('.note-discussion .js-note-text').trigger(upArrowEvent);
-
- expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit');
-
- done();
- });
- });
- });
- });
-
- describe('Changes tab with diff comments', () => {
- beforeEach(() => {
- const diffsResponse = getJSONFixture(changesTabJsonFixture);
- const noteFormHtml = `<form class="js-new-note-form">
- <textarea class="js-note-text"></textarea>
- </form>`;
- setFixtures(diffsResponse.html + noteFormHtml);
- $('body').attr('data-page', 'projects:merge_requests:show');
- window.gon.current_user_id = $('.note:last').data('authorId');
-
- return new Notes('', []);
- });
-
- afterEach(() => {
- // Undo what we did to the shared <body>
- $('body').removeAttr('data-page');
- });
-
- describe('up arrow', () => {
- it('edits last comment in discussion when triggered in discussion form', (done) => {
- const upArrowEvent = $.Event('keydown');
- upArrowEvent.which = upArrowKeyCode;
-
- spyOnEvent('.note:last .js-note-edit', 'click');
-
- $('.js-discussion-reply-button').trigger('click');
-
- setTimeout(() => {
- $('.js-note-text').trigger(upArrowEvent);
-
- expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit');
-
- done();
- });
- });
- });
- });
-});
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 3dbd9756cd2..08928e13985 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,5 +1,4 @@
-/* eslint-disable no-var, comma-dangle, object-shorthand */
-
+/* eslint-disable no-var, object-shorthand */
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
@@ -7,480 +6,228 @@ import MergeRequestTabs from '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
import '~/breakpoints';
import '~/lib/utils/common_utils';
-import Diff from '~/diff';
-import Notes from '~/notes';
import 'vendor/jquery.scrollTo';
-
-(function () {
- describe('MergeRequestTabs', function () {
- var stubLocation = {};
- var setLocation = function (stubs) {
- var defaults = {
- pathname: '',
- search: '',
- hash: ''
- };
- $.extend(stubLocation, defaults, stubs || {});
+import initMrPage from './helpers/init_vue_mr_page_helper';
+
+describe('MergeRequestTabs', function() {
+ let mrPageMock;
+ var stubLocation = {};
+ var setLocation = function(stubs) {
+ var defaults = {
+ pathname: '',
+ search: '',
+ hash: '',
};
+ $.extend(stubLocation, defaults, stubs || {});
+ };
- const inlineChangesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json';
- const parallelChangesTabJsonFixture = 'merge_request_diffs/parallel_changes_tab_with_comments.json';
- preloadFixtures(
- 'merge_requests/merge_request_with_task_list.html.raw',
- 'merge_requests/diff_comment.html.raw',
- inlineChangesTabJsonFixture,
- parallelChangesTabJsonFixture
- );
-
- beforeEach(function () {
- this.class = new MergeRequestTabs({ stubLocation: stubLocation });
- setLocation();
-
- this.spies = {
- history: spyOn(window.history, 'replaceState').and.callFake(function () {})
- };
- });
-
- afterEach(function () {
- this.class.unbindEvents();
- this.class.destroyPipelinesView();
- });
-
- describe('activateTab', function () {
- beforeEach(function () {
- spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
- loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
- this.subject = this.class.activateTab;
- });
- it('shows the notes tab when action is show', function () {
- this.subject('show');
- expect($('#notes')).toHaveClass('active');
- });
- it('shows the commits tab when action is commits', function () {
- this.subject('commits');
- expect($('#commits')).toHaveClass('active');
- });
- it('shows the diffs tab when action is diffs', function () {
- this.subject('diffs');
- expect($('#diffs')).toHaveClass('active');
- });
- });
-
- describe('opensInNewTab', function () {
- var tabUrl;
- var windowTarget = '_blank';
-
- beforeEach(function () {
- loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
-
- tabUrl = $('.commits-tab a').attr('href');
+ preloadFixtures(
+ 'merge_requests/merge_request_with_task_list.html.raw',
+ 'merge_requests/diff_comment.html.raw',
+ );
- spyOn($.fn, 'attr').and.returnValue(tabUrl);
- });
-
- describe('meta click', () => {
- let metakeyEvent;
- beforeEach(function () {
- metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true });
- });
+ beforeEach(function() {
+ mrPageMock = initMrPage();
+ this.class = new MergeRequestTabs({ stubLocation: stubLocation });
+ setLocation();
- it('opens page when commits link is clicked', function () {
- spyOn(window, 'open').and.callFake(function (url, name) {
- expect(url).toEqual(tabUrl);
- expect(name).toEqual(windowTarget);
- });
+ this.spies = {
+ history: spyOn(window.history, 'replaceState').and.callFake(function() {}),
+ };
+ });
- this.class.bindEvents();
- $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent);
- });
+ afterEach(function() {
+ this.class.unbindEvents();
+ this.class.destroyPipelinesView();
+ mrPageMock.restore();
+ });
- it('opens page when commits badge is clicked', function () {
- spyOn(window, 'open').and.callFake(function (url, name) {
- expect(url).toEqual(tabUrl);
- expect(name).toEqual(windowTarget);
- });
+ describe('opensInNewTab', function() {
+ var tabUrl;
+ var windowTarget = '_blank';
- this.class.bindEvents();
- $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent);
- });
- });
+ beforeEach(function() {
+ loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
- it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () {
- spyOn(window, 'open').and.callFake(function (url, name) {
- expect(url).toEqual(tabUrl);
- expect(name).toEqual(windowTarget);
- });
+ tabUrl = $('.commits-tab a').attr('href');
+ });
- this.class.clickTab({
- metaKey: false,
- ctrlKey: true,
- which: 1,
- stopImmediatePropagation: function () {}
- });
+ describe('meta click', () => {
+ let metakeyEvent;
+ beforeEach(function() {
+ metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true });
});
- it('opens page tab in a new browser tab with Cmd+Click - Mac', function () {
- spyOn(window, 'open').and.callFake(function (url, name) {
+ it('opens page when commits link is clicked', function() {
+ spyOn(window, 'open').and.callFake(function(url, name) {
expect(url).toEqual(tabUrl);
expect(name).toEqual(windowTarget);
});
- this.class.clickTab({
- metaKey: true,
- ctrlKey: false,
- which: 1,
- stopImmediatePropagation: function () {}
- });
+ this.class.bindEvents();
+ $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent);
});
- it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () {
- spyOn(window, 'open').and.callFake(function (url, name) {
+ it('opens page when commits badge is clicked', function() {
+ spyOn(window, 'open').and.callFake(function(url, name) {
expect(url).toEqual(tabUrl);
expect(name).toEqual(windowTarget);
});
- this.class.clickTab({
- metaKey: false,
- ctrlKey: false,
- which: 2,
- stopImmediatePropagation: function () {}
- });
+ this.class.bindEvents();
+ $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent);
});
});
- describe('setCurrentAction', function () {
- beforeEach(function () {
- spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
- this.subject = this.class.setCurrentAction;
- });
-
- it('changes from commits', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1/commits'
- });
- expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
- expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
- });
-
- it('changes from diffs', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1/diffs'
- });
-
- expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
- expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function() {
+ spyOn(window, 'open').and.callFake(function(url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual(windowTarget);
});
- it('changes from diffs.html', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1/diffs.html'
- });
- expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
- expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: true,
+ which: 1,
+ stopImmediatePropagation: function() {},
});
+ });
- it('changes from notes', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1'
- });
- expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
- expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ it('opens page tab in a new browser tab with Cmd+Click - Mac', function() {
+ spyOn(window, 'open').and.callFake(function(url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual(windowTarget);
});
- it('includes search parameters and hash string', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1/diffs',
- search: '?view=parallel',
- hash: '#L15-35'
- });
- expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35');
+ this.class.clickTab({
+ metaKey: true,
+ ctrlKey: false,
+ which: 1,
+ stopImmediatePropagation: function() {},
});
+ });
- it('replaces the current history state', function () {
- var newState;
- setLocation({
- pathname: '/foo/bar/merge_requests/1'
- });
- newState = this.subject('commits');
- expect(this.spies.history).toHaveBeenCalledWith({
- url: newState
- }, document.title, newState);
+ it('opens page tab in a new browser tab with Middle-click - Mac/PC', function() {
+ spyOn(window, 'open').and.callFake(function(url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual(windowTarget);
});
- it('treats "show" like "notes"', function () {
- setLocation({
- pathname: '/foo/bar/merge_requests/1/commits'
- });
- expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: false,
+ which: 2,
+ stopImmediatePropagation: function() {},
});
});
+ });
- describe('tabShown', () => {
- let mock;
+ describe('setCurrentAction', function() {
+ let mock;
- beforeEach(function () {
- mock = new MockAdapter(axios);
- mock.onGet(/(.*)\/diffs\.json/).reply(200, {
- data: { html: '' },
- });
+ beforeEach(function() {
+ mock = new MockAdapter(axios);
+ mock.onAny().reply({ data: {} });
+ this.subject = this.class.setCurrentAction;
+ });
- loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
- });
+ afterEach(() => {
+ mock.restore();
+ });
- afterEach(() => {
- mock.restore();
+ it('changes from commits', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1/commits',
});
- describe('with "Side-by-side"/parallel diff view', () => {
- beforeEach(function () {
- this.class.diffViewType = () => 'parallel';
- Diff.prototype.diffViewType = () => 'parallel';
- });
-
- it('maintains `container-limited` for pipelines tab', function (done) {
- const asyncClick = function (selector) {
- return new Promise((resolve) => {
- setTimeout(() => {
- document.querySelector(selector).click();
- resolve();
- });
- });
- };
- asyncClick('.merge-request-tabs .pipelines-tab a')
- .then(() => asyncClick('.merge-request-tabs .diffs-tab a'))
- .then(() => asyncClick('.merge-request-tabs .pipelines-tab a'))
- .then(() => {
- const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited');
- expect(hasContainerLimitedClass).toBe(true);
- })
- .then(done)
- .catch((err) => {
- done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`);
- });
- });
-
- it('maintains `container-limited` when switching from "Changes" tab before it loads', function (done) {
- const asyncClick = function (selector) {
- return new Promise((resolve) => {
- setTimeout(() => {
- document.querySelector(selector).click();
- resolve();
- });
- });
- };
-
- asyncClick('.merge-request-tabs .diffs-tab a')
- .then(() => asyncClick('.merge-request-tabs .notes-tab a'))
- .then(() => {
- const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited');
- expect(hasContainerLimitedClass).toBe(true);
- })
- .then(done)
- .catch((err) => {
- done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`);
- });
- });
- });
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
});
- describe('loadDiff', function () {
- beforeEach(() => {
- loadFixtures('merge_requests/diff_comment.html.raw');
- $('body').attr('data-page', 'projects:merge_requests:show');
- window.gl.ImageFile = () => {};
- Notes.initialize('', []);
- spyOn(Notes.instance, 'toggleDiffNote').and.callThrough();
+ it('changes from diffs', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1/diffs',
});
- afterEach(() => {
- delete window.gl.ImageFile;
- delete window.notes;
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ });
- // Undo what we did to the shared <body>
- $('body').removeAttr('data-page');
+ it('changes from diffs.html', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1/diffs.html',
});
- it('triggers Ajax request to JSON endpoint', function (done) {
- const url = '/foo/bar/merge_requests/1/diffs';
-
- spyOn(axios, 'get').and.callFake((reqUrl) => {
- expect(reqUrl).toBe(`${url}.json`);
-
- done();
-
- return Promise.resolve({ data: {} });
- });
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ });
- this.class.loadDiff(url);
+ it('changes from notes', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1',
});
- it('triggers scroll event when diff already loaded', function (done) {
- spyOn(axios, 'get').and.callFake(done.fail);
- spyOn(document, 'dispatchEvent');
-
- this.class.diffsLoaded = true;
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+ expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
+ expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ });
- expect(
- document.dispatchEvent,
- ).toHaveBeenCalledWith(new CustomEvent('scroll'));
- done();
+ it('includes search parameters and hash string', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1/diffs',
+ search: '?view=parallel',
+ hash: '#L15-35',
});
- describe('with inline diff', () => {
- let noteId;
- let noteLineNumId;
- let mock;
-
- beforeEach(() => {
- const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture);
-
- const $html = $(diffsResponse.html);
- noteId = $html.find('.note').attr('id');
- noteLineNumId = $html
- .find('.note')
- .closest('.notes_holder')
- .prev('.line_holder')
- .find('a[data-linenumber]')
- .attr('href')
- .replace('#', '');
-
- mock = new MockAdapter(axios);
- mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('with note fragment hash', () => {
- it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId);
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
-
- setTimeout(() => {
- expect(noteId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
- target: jasmine.any(Object),
- lineType: 'old',
- forceShow: true,
- });
-
- done();
- });
- });
-
- it('should gracefully ignore non-existant fragment hash', function (done) {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
-
- setTimeout(() => {
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
-
- done();
- });
- });
- });
-
- describe('with line number fragment hash', () => {
- it('should gracefully ignore line number fragment hash', function () {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId);
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35');
+ });
- expect(noteLineNumId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
- });
- });
+ it('replaces the current history state', function() {
+ var newState;
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1',
});
+ newState = this.subject('commits');
- describe('with parallel diff', () => {
- let noteId;
- let noteLineNumId;
- let mock;
-
- beforeEach(() => {
- const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture);
-
- const $html = $(diffsResponse.html);
- noteId = $html.find('.note').attr('id');
- noteLineNumId = $html
- .find('.note')
- .closest('.notes_holder')
- .prev('.line_holder')
- .find('a[data-linenumber]')
- .attr('href')
- .replace('#', '');
-
- mock = new MockAdapter(axios);
- mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('with note fragment hash', () => {
- it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId);
-
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
-
- setTimeout(() => {
- expect(noteId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
- target: jasmine.any(Object),
- lineType: 'new',
- forceShow: true,
- });
-
- done();
- });
- });
-
- it('should gracefully ignore non-existant fragment hash', function (done) {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
-
- setTimeout(() => {
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
- done();
- });
- });
- });
-
- describe('with line number fragment hash', () => {
- it('should gracefully ignore line number fragment hash', function () {
- spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId);
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+ expect(this.spies.history).toHaveBeenCalledWith(
+ {
+ url: newState,
+ },
+ document.title,
+ newState,
+ );
+ });
- expect(noteLineNumId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
- });
- });
+ it('treats "show" like "notes"', function() {
+ setLocation({
+ pathname: '/foo/bar/merge_requests/1/commits',
});
+
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
});
+ });
- describe('expandViewContainer', function () {
- beforeEach(() => {
- $('body').append('<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>');
- });
+ describe('expandViewContainer', function() {
+ beforeEach(() => {
+ $('body').append(
+ '<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>',
+ );
+ });
- afterEach(() => {
- $('.content-wrapper').remove();
- });
+ afterEach(() => {
+ $('.content-wrapper').remove();
+ });
- it('removes container-limited from containers', function () {
- this.class.expandViewContainer();
+ it('removes container-limited from containers', function() {
+ this.class.expandViewContainer();
- expect($('.content-wrapper')).not.toContainElement('.container-limited');
- });
+ expect($('.content-wrapper')).not.toContainElement('.container-limited');
+ });
- it('does remove container-limited from breadcrumbs', function () {
- $('.container-limited').addClass('breadcrumbs');
- this.class.expandViewContainer();
+ it('does remove container-limited from breadcrumbs', function() {
+ $('.container-limited').addClass('breadcrumbs');
+ this.class.expandViewContainer();
- expect($('.content-wrapper')).toContainElement('.container-limited');
- });
+ expect($('.content-wrapper')).toContainElement('.container-limited');
});
});
-}).call(window);
+});
diff --git a/spec/javascripts/namespace_select_spec.js b/spec/javascripts/namespace_select_spec.js
index 3b2641f7646..07b82ce721e 100644
--- a/spec/javascripts/namespace_select_spec.js
+++ b/spec/javascripts/namespace_select_spec.js
@@ -22,7 +22,7 @@ describe('NamespaceSelect', () => {
const dropdown = document.createElement('div');
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
- glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ [glDropdownOptions] = $.fn.glDropdown.calls.argsFor(0);
});
it('prevents click events', () => {
@@ -43,7 +43,7 @@ describe('NamespaceSelect', () => {
dropdown.dataset.isFilter = 'true';
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
- glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ [glDropdownOptions] = $.fn.glDropdown.calls.argsFor(0);
});
it('does not prevent click events', () => {
diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js
index 8f8ba231ae8..0b1b11de1fd 100644
--- a/spec/javascripts/notebook/cells/markdown_spec.js
+++ b/spec/javascripts/notebook/cells/markdown_spec.js
@@ -14,6 +14,7 @@ describe('Markdown component', () => {
beforeEach((done) => {
json = getJSONFixture('blob/notebook/basic.json');
+ // eslint-disable-next-line prefer-destructuring
cell = json.cells[1];
vm = new Component({
diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js
index a7d1e4331eb..155c91dcc46 100644
--- a/spec/javascripts/notes/components/comment_form_spec.js
+++ b/spec/javascripts/notes/components/comment_form_spec.js
@@ -1,23 +1,27 @@
import $ from 'jquery';
import Vue from 'vue';
import Autosize from 'autosize';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import CommentForm from '~/notes/components/comment_form.vue';
+import * as constants from '~/notes/constants';
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_comment_form component', () => {
+ let store;
let vm;
const Component = Vue.extend(CommentForm);
let mountComponent;
beforeEach(() => {
- mountComponent = (noteableType = 'issue') => new Component({
- propsData: {
- noteableType,
- },
- store,
- }).$mount();
+ store = createStore();
+ mountComponent = (noteableType = 'issue') =>
+ new Component({
+ propsData: {
+ noteableType,
+ },
+ store,
+ }).$mount();
});
afterEach(() => {
@@ -34,7 +38,9 @@ describe('issue_comment_form component', () => {
});
it('should render user avatar with link', () => {
- expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path);
+ expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(
+ userDataMock.path,
+ );
});
describe('handleSave', () => {
@@ -60,7 +66,7 @@ describe('issue_comment_form component', () => {
expect(vm.toggleIssueState).toHaveBeenCalled();
});
- it('should disable action button whilst submitting', (done) => {
+ it('should disable action button whilst submitting', done => {
const saveNotePromise = Promise.resolve();
vm.note = 'hello world';
spyOn(vm, 'saveNote').and.returnValue(saveNotePromise);
@@ -87,16 +93,18 @@ describe('issue_comment_form component', () => {
).toEqual('Write a comment or drag your files here…');
});
- it('should make textarea disabled while requesting', (done) => {
+ it('should make textarea disabled while requesting', done => {
const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button'));
vm.note = 'hello world';
spyOn(vm, 'stopPolling');
spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {}));
- vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton.
+ vm.$nextTick(() => {
+ // Wait for vm.note change triggered. It should enable $submitButton.
$submitButton.trigger('click');
- vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea.
+ vm.$nextTick(() => {
+ // Wait for vm.isSubmitting triggered. It should disable textarea.
expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy();
done();
});
@@ -105,21 +113,27 @@ describe('issue_comment_form component', () => {
it('should support quick actions', () => {
expect(
- vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'),
+ vm.$el
+ .querySelector('.js-main-target-form textarea')
+ .getAttribute('data-supports-quick-actions'),
).toEqual('true');
});
it('should link to markdown docs', () => {
const { markdownDocsPath } = notesDataMock;
- expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown');
+ expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual(
+ 'Markdown',
+ );
});
it('should link to quick actions docs', () => {
const { quickActionsDocsPath } = notesDataMock;
- expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions');
+ expect(
+ vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim(),
+ ).toEqual('quick actions');
});
- it('should resize textarea after note discarded', (done) => {
+ it('should resize textarea after note discarded', done => {
spyOn(Autosize, 'update');
spyOn(vm, 'discard').and.callThrough();
@@ -136,7 +150,9 @@ describe('issue_comment_form component', () => {
it('should enter edit mode when arrow up is pressed', () => {
spyOn(vm, 'editCurrentUserLastNote').and.callThrough();
vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
- vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(38, true));
+ vm.$el
+ .querySelector('.js-main-target-form textarea')
+ .dispatchEvent(keyboardDownEvent(38, true));
expect(vm.editCurrentUserLastNote).toHaveBeenCalled();
});
@@ -151,7 +167,9 @@ describe('issue_comment_form component', () => {
it('should save note when cmd+enter is pressed', () => {
spyOn(vm, 'handleSave').and.callThrough();
vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
- vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true));
+ vm.$el
+ .querySelector('.js-main-target-form textarea')
+ .dispatchEvent(keyboardDownEvent(13, true));
expect(vm.handleSave).toHaveBeenCalled();
});
@@ -159,7 +177,9 @@ describe('issue_comment_form component', () => {
it('should save note when ctrl+enter is pressed', () => {
spyOn(vm, 'handleSave').and.callThrough();
vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
- vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, false, true));
+ vm.$el
+ .querySelector('.js-main-target-form textarea')
+ .dispatchEvent(keyboardDownEvent(13, false, true));
expect(vm.handleSave).toHaveBeenCalled();
});
@@ -168,41 +188,51 @@ describe('issue_comment_form component', () => {
describe('actions', () => {
it('should be possible to close the issue', () => {
- expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close issue');
+ expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual(
+ 'Close issue',
+ );
});
it('should render comment button as disabled', () => {
- expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual('disabled');
+ expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual(
+ 'disabled',
+ );
});
- it('should enable comment button if it has note', (done) => {
+ it('should enable comment button if it has note', done => {
vm.note = 'Foo';
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual(null);
+ expect(
+ vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled'),
+ ).toEqual(null);
done();
});
});
- it('should update buttons texts when it has note', (done) => {
+ it('should update buttons texts when it has note', done => {
vm.note = 'Foo';
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Comment & close issue');
+ expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual(
+ 'Comment & close issue',
+ );
expect(vm.$el.querySelector('.js-note-discard')).toBeDefined();
done();
});
});
- it('updates button text with noteable type', (done) => {
- vm.noteableType = 'merge_request';
+ it('updates button text with noteable type', done => {
+ vm.noteableType = constants.MERGE_REQUEST_NOTEABLE_TYPE;
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close merge request');
+ expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual(
+ 'Close merge request',
+ );
done();
});
});
describe('when clicking close/reopen button', () => {
- it('should disable button and show a loading spinner', (done) => {
+ it('should disable button and show a loading spinner', done => {
const toggleStateButton = vm.$el.querySelector('.js-action-button');
toggleStateButton.click();
@@ -217,7 +247,7 @@ describe('issue_comment_form component', () => {
});
describe('issue is confidential', () => {
- it('shows information warning', (done) => {
+ it('shows information warning', done => {
store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true }));
Vue.nextTick(() => {
expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined();
@@ -237,7 +267,9 @@ describe('issue_comment_form component', () => {
});
it('should render signed out widget', () => {
- expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply');
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual(
+ 'Please register or sign in to reply',
+ );
});
it('should not render submission form', () => {
diff --git a/spec/javascripts/notes/components/diff_file_header_spec.js b/spec/javascripts/notes/components/diff_file_header_spec.js
deleted file mode 100644
index ef6d513444a..00000000000
--- a/spec/javascripts/notes/components/diff_file_header_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import Vue from 'vue';
-import DiffFileHeader from '~/notes/components/diff_file_header.vue';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const discussionFixture = 'merge_requests/diff_discussion.json';
-
-describe('diff_file_header', () => {
- let vm;
- const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
- const diffFile = convertObjectPropsToCamelCase(diffDiscussionMock.diff_file);
- const props = {
- diffFile,
- };
- const Component = Vue.extend(DiffFileHeader);
- const selectors = {
- get copyButton() {
- return vm.$el.querySelector('button[data-original-title="Copy file path to clipboard"]');
- },
- get fileName() {
- return vm.$el.querySelector('.file-title-name');
- },
- get titleWrapper() {
- return vm.$refs.titleWrapper;
- },
- };
-
- describe('submodule', () => {
- beforeEach(() => {
- props.diffFile.submodule = true;
- props.diffFile.submoduleLink = '<a href="/bha">Submodule</a>';
-
- vm = mountComponent(Component, props);
- });
-
- it('shows submoduleLink', () => {
- expect(selectors.fileName.innerHTML).toBe(props.diffFile.submoduleLink);
- });
-
- it('has button to copy blob path', () => {
- expect(selectors.copyButton).toExist();
- expect(selectors.copyButton.getAttribute('data-clipboard-text')).toBe(props.diffFile.submoduleLink);
- });
- });
-
- describe('changed file', () => {
- beforeEach(() => {
- props.diffFile.submodule = false;
- props.diffFile.discussionPath = 'some/discussion/id';
-
- vm = mountComponent(Component, props);
- });
-
- it('shows file type icon', () => {
- expect(vm.$el.innerHTML).toContain('fa-file-text-o');
- });
-
- it('links to discussion path', () => {
- expect(selectors.titleWrapper).toExist();
- expect(selectors.titleWrapper.tagName).toBe('A');
- expect(selectors.titleWrapper.getAttribute('href')).toBe(props.diffFile.discussionPath);
- });
-
- it('shows plain title if no link given', () => {
- props.diffFile.discussionPath = undefined;
- vm = mountComponent(Component, props);
-
- expect(selectors.titleWrapper.tagName).not.toBe('A');
- expect(selectors.titleWrapper.href).toBeFalsy();
- });
-
- it('has button to copy file path', () => {
- expect(selectors.copyButton).toExist();
- expect(selectors.copyButton.getAttribute('data-clipboard-text')).toBe(props.diffFile.filePath);
- });
-
- it('shows file mode change', (done) => {
- vm.diffFile = {
- ...props.diffFile,
- modeChanged: true,
- aMode: '100755',
- bMode: '100644',
- };
-
- Vue.nextTick(() => {
- expect(
- vm.$refs.fileMode.textContent.trim(),
- ).toBe('100755 → 100644');
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js
index f4ec7132dbd..239d7950907 100644
--- a/spec/javascripts/notes/components/diff_with_note_spec.js
+++ b/spec/javascripts/notes/components/diff_with_note_spec.js
@@ -1,12 +1,14 @@
import Vue from 'vue';
import DiffWithNote from '~/notes/components/diff_with_note.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import createStore from '~/notes/stores';
+import { mountComponentWithStore } from 'spec/helpers';
const discussionFixture = 'merge_requests/diff_discussion.json';
const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json';
describe('diff_with_note', () => {
+ let store;
let vm;
const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
const diffDiscussion = convertObjectPropsToCamelCase(diffDiscussionMock);
@@ -29,9 +31,21 @@ describe('diff_with_note', () => {
},
};
+ beforeEach(() => {
+ store = createStore();
+ store.replaceState({
+ ...store.state,
+ notes: {
+ noteableData: {
+ current_user: {},
+ },
+ },
+ });
+ });
+
describe('text diff', () => {
beforeEach(() => {
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
});
it('shows text diff', () => {
@@ -55,7 +69,7 @@ describe('diff_with_note', () => {
});
it('shows image diff', () => {
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(selectors.container).toHaveClass('js-image-file');
expect(selectors.diffTable).not.toExist();
diff --git a/spec/javascripts/notes/components/discussion_counter_spec.js b/spec/javascripts/notes/components/discussion_counter_spec.js
new file mode 100644
index 00000000000..7b2302e6f47
--- /dev/null
+++ b/spec/javascripts/notes/components/discussion_counter_spec.js
@@ -0,0 +1,58 @@
+import Vue from 'vue';
+import createStore from '~/notes/stores';
+import DiscussionCounter from '~/notes/components/discussion_counter.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
+
+describe('DiscussionCounter component', () => {
+ let store;
+ let vm;
+
+ beforeEach(() => {
+ window.mrTabs = {};
+
+ const Component = Vue.extend(DiscussionCounter);
+
+ store = createStore();
+ store.dispatch('setNoteableData', noteableDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+
+ vm = createComponentWithStore(Component, store);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('methods', () => {
+ describe('jumpToFirstUnresolvedDiscussion', () => {
+ it('expands unresolved discussion', () => {
+ spyOn(vm, 'expandDiscussion').and.stub();
+ const discussions = [
+ {
+ ...discussionMock,
+ id: discussionMock.id,
+ notes: [{ ...discussionMock.notes[0], resolved: true }],
+ },
+ {
+ ...discussionMock,
+ id: discussionMock.id + 1,
+ notes: [{ ...discussionMock.notes[0], resolved: false }],
+ },
+ ];
+ const firstDiscussionId = discussionMock.id + 1;
+ store.replaceState({
+ ...store.state,
+ discussions,
+ });
+ setFixtures(`
+ <div data-discussion-id="${firstDiscussionId}"></div>
+ `);
+
+ vm.jumpToFirstUnresolvedDiscussion();
+
+ expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: firstDiscussionId });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js
index c9e549d2096..52cc42cb53d 100644
--- a/spec/javascripts/notes/components/note_actions_spec.js
+++ b/spec/javascripts/notes/components/note_actions_spec.js
@@ -1,14 +1,16 @@
import Vue from 'vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import noteActions from '~/notes/components/note_actions.vue';
import { userDataMock } from '../mock_data';
describe('issue_note_actions component', () => {
let vm;
+ let store;
let Component;
beforeEach(() => {
Component = Vue.extend(noteActions);
+ store = createStore();
});
afterEach(() => {
@@ -27,7 +29,9 @@ describe('issue_note_actions component', () => {
canAwardEmoji: true,
canReportAsAbuse: true,
noteId: 539,
- reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
+ noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1',
+ reportAbusePath:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
};
store.dispatch('setUserData', userDataMock);
@@ -74,7 +78,9 @@ describe('issue_note_actions component', () => {
canAwardEmoji: false,
canReportAsAbuse: false,
noteId: 539,
- reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
+ noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1',
+ reportAbusePath:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
};
vm = new Component({
store,
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index d494c63ff11..7eb4d3aed29 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -3,7 +3,9 @@ import _ from 'underscore';
import Vue from 'vue';
import notesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service';
+import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm';
+import { mountComponentWithStore } from 'spec/helpers';
import * as mockData from '../mock_data';
const vueMatchers = {
@@ -22,6 +24,7 @@ const vueMatchers = {
describe('note_app', () => {
let mountComponent;
let vm;
+ let store;
beforeEach(() => {
jasmine.addMatchers(vueMatchers);
@@ -29,16 +32,18 @@ describe('note_app', () => {
const IssueNotesApp = Vue.extend(notesApp);
- mountComponent = (data) => {
+ store = createStore();
+ mountComponent = data => {
const props = data || {
noteableData: mockData.noteableDataMock,
notesData: mockData.notesDataMock,
userData: mockData.userDataMock,
};
- return new IssueNotesApp({
- propsData: props,
- }).$mount();
+ return mountComponentWithStore(IssueNotesApp, {
+ props,
+ store,
+ });
};
});
@@ -48,9 +53,11 @@ describe('note_app', () => {
describe('set data', () => {
const responseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
+ next(
+ request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }),
+ );
};
beforeEach(() => {
@@ -74,8 +81,8 @@ describe('note_app', () => {
expect(vm.$store.state.userData).toEqual(mockData.userDataMock);
});
- it('should fetch notes', () => {
- expect(vm.$store.state.notes).toEqual([]);
+ it('should fetch discussions', () => {
+ expect(vm.$store.state.discussions).toEqual([]);
});
});
@@ -89,15 +96,20 @@ describe('note_app', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
});
- it('should render list of notes', (done) => {
- const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET['/gitlab-org/gitlab-ce/issues/26/discussions.json'][0].notes[0];
+ it('should render list of notes', done => {
+ const note =
+ mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
+ '/gitlab-org/gitlab-ce/issues/26/discussions.json'
+ ][0].notes[0];
setTimeout(() => {
expect(
vm.$el.querySelector('.main-notes-list .note-header-author-name').textContent.trim(),
).toEqual(note.author.name);
- expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual(note.note_html);
+ expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual(
+ note.note_html,
+ );
done();
}, 0);
});
@@ -110,9 +122,9 @@ describe('note_app', () => {
});
it('should render form comment button as disabled', () => {
- expect(
- vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled'),
- ).toEqual('disabled');
+ expect(vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled')).toEqual(
+ 'disabled',
+ );
});
});
@@ -135,7 +147,7 @@ describe('note_app', () => {
describe('update note', () => {
describe('individual note', () => {
- beforeEach((done) => {
+ beforeEach(done => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent();
@@ -156,7 +168,7 @@ describe('note_app', () => {
expect(vm).toIncludeElement('.js-vue-issue-note-form');
});
- it('calls the service to update the note', (done) => {
+ it('calls the service to update the note', done => {
vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
vm.$el.querySelector('.js-vue-issue-save').click();
@@ -169,7 +181,7 @@ describe('note_app', () => {
});
describe('discussion note', () => {
- beforeEach((done) => {
+ beforeEach(done => {
Vue.http.interceptors.push(mockData.discussionNoteInterceptor);
spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent();
@@ -191,7 +203,7 @@ describe('note_app', () => {
expect(vm).toIncludeElement('.js-vue-issue-note-form');
});
- it('updates the note and resets the edit form', (done) => {
+ it('updates the note and resets the edit form', done => {
vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
vm.$el.querySelector('.js-vue-issue-save').click();
@@ -211,12 +223,16 @@ describe('note_app', () => {
it('should render markdown docs url', () => {
const { markdownDocsPath } = mockData.notesDataMock;
- expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown');
+ expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual(
+ 'Markdown',
+ );
});
it('should render quick action docs url', () => {
const { quickActionsDocsPath } = mockData.notesDataMock;
- expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions');
+ expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual(
+ 'quick actions',
+ );
});
});
@@ -230,7 +246,7 @@ describe('note_app', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
});
- it('should render markdown docs url', (done) => {
+ it('should render markdown docs url', done => {
setTimeout(() => {
vm.$el.querySelector('.js-note-edit').click();
const { markdownDocsPath } = mockData.notesDataMock;
@@ -244,15 +260,15 @@ describe('note_app', () => {
}, 0);
});
- it('should not render quick actions docs url', (done) => {
+ it('should not render quick actions docs url', done => {
setTimeout(() => {
vm.$el.querySelector('.js-note-edit').click();
const { quickActionsDocsPath } = mockData.notesDataMock;
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`),
- ).toEqual(null);
+ expect(vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`)).toEqual(
+ null,
+ );
done();
});
}, 0);
diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js
index 1c30d8691b1..9d98ba219da 100644
--- a/spec/javascripts/notes/components/note_awards_list_spec.js
+++ b/spec/javascripts/notes/components/note_awards_list_spec.js
@@ -1,15 +1,17 @@
import Vue from 'vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import awardsNote from '~/notes/components/note_awards_list.vue';
import { noteableDataMock, notesDataMock } from '../mock_data';
describe('note_awards_list component', () => {
+ let store;
let vm;
let awardsMock;
beforeEach(() => {
const Component = Vue.extend(awardsNote);
+ store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
awardsMock = [
@@ -41,7 +43,9 @@ describe('note_awards_list component', () => {
it('should render awarded emojis', () => {
expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined();
- expect(vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]')).toBeDefined();
+ expect(
+ vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]'),
+ ).toBeDefined();
});
it('should be possible to remove awarded emoji', () => {
diff --git a/spec/javascripts/notes/components/note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js
index 4e551496ff0..efad0785afe 100644
--- a/spec/javascripts/notes/components/note_body_spec.js
+++ b/spec/javascripts/notes/components/note_body_spec.js
@@ -1,15 +1,16 @@
-
import Vue from 'vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import noteBody from '~/notes/components/note_body.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note_body component', () => {
+ let store;
let vm;
beforeEach(() => {
const Component = Vue.extend(noteBody);
+ store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
@@ -37,7 +38,7 @@ describe('issue_note_body component', () => {
});
describe('isEditing', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.isEditing = true;
Vue.nextTick(done);
});
diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js
index 413d4f69434..95d400ab3df 100644
--- a/spec/javascripts/notes/components/note_form_spec.js
+++ b/spec/javascripts/notes/components/note_form_spec.js
@@ -1,16 +1,18 @@
import Vue from 'vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import issueNoteForm from '~/notes/components/note_form.vue';
import { noteableDataMock, notesDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_note_form component', () => {
+ let store;
let vm;
let props;
beforeEach(() => {
const Component = Vue.extend(issueNoteForm);
+ store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
@@ -31,14 +33,18 @@ describe('issue_note_form component', () => {
});
describe('conflicts editing', () => {
- it('should show conflict message if note changes outside the component', (done) => {
+ it('should show conflict message if note changes outside the component', done => {
vm.isEditing = true;
vm.noteBody = 'Foo';
- const message = 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.';
+ const message =
+ 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.';
Vue.nextTick(() => {
expect(
- vm.$el.querySelector('.js-conflict-edit-warning').textContent.replace(/\s+/g, ' ').trim(),
+ vm.$el
+ .querySelector('.js-conflict-edit-warning')
+ .textContent.replace(/\s+/g, ' ')
+ .trim(),
).toEqual(message);
done();
});
@@ -47,14 +53,16 @@ describe('issue_note_form component', () => {
describe('form', () => {
it('should render text area with placeholder', () => {
- expect(
- vm.$el.querySelector('textarea').getAttribute('placeholder'),
- ).toEqual('Write a comment or drag your files here…');
+ expect(vm.$el.querySelector('textarea').getAttribute('placeholder')).toEqual(
+ 'Write a comment or drag your files here…',
+ );
});
it('should link to markdown docs', () => {
const { markdownDocsPath } = notesDataMock;
- expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown');
+ expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual(
+ 'Markdown',
+ );
});
describe('keyboard events', () => {
@@ -87,7 +95,7 @@ describe('issue_note_form component', () => {
});
describe('actions', () => {
- it('should be possible to cancel', (done) => {
+ it('should be possible to cancel', done => {
spyOn(vm, 'cancelHandler').and.callThrough();
vm.isEditing = true;
@@ -101,7 +109,7 @@ describe('issue_note_form component', () => {
});
});
- it('should be possible to update the note', (done) => {
+ it('should be possible to update the note', done => {
vm.isEditing = true;
Vue.nextTick(() => {
diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js
index 5636f8d1a9f..a3c6bf78988 100644
--- a/spec/javascripts/notes/components/note_header_spec.js
+++ b/spec/javascripts/notes/components/note_header_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import noteHeader from '~/notes/components/note_header.vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
describe('note_header component', () => {
+ let store;
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(noteHeader);
+ store = createStore();
});
afterEach(() => {
@@ -38,12 +40,8 @@ describe('note_header component', () => {
});
it('should render user information', () => {
- expect(
- vm.$el.querySelector('.note-header-author-name').textContent.trim(),
- ).toEqual('Root');
- expect(
- vm.$el.querySelector('.note-header-info a').getAttribute('href'),
- ).toEqual('/root');
+ expect(vm.$el.querySelector('.note-header-author-name').textContent.trim()).toEqual('Root');
+ expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual('/root');
});
it('should render timestamp link', () => {
@@ -78,7 +76,7 @@ describe('note_header component', () => {
expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined();
});
- it('emits toggle event on click', (done) => {
+ it('emits toggle event on click', done => {
spyOn(vm, '$emit');
vm.$el.querySelector('.js-vue-toggle-button').click();
@@ -89,24 +87,24 @@ describe('note_header component', () => {
});
});
- it('renders up arrow when open', (done) => {
+ it('renders up arrow when open', done => {
vm.expanded = true;
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.js-vue-toggle-button i').classList,
- ).toContain('fa-chevron-up');
+ expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain(
+ 'fa-chevron-up',
+ );
done();
});
});
- it('renders down arrow when closed', (done) => {
+ it('renders down arrow when closed', done => {
vm.expanded = false;
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.js-vue-toggle-button i').classList,
- ).toContain('fa-chevron-down');
+ expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain(
+ 'fa-chevron-down',
+ );
done();
});
});
diff --git a/spec/javascripts/notes/components/note_signed_out_widget_spec.js b/spec/javascripts/notes/components/note_signed_out_widget_spec.js
index 6cba8053888..e217a2caa73 100644
--- a/spec/javascripts/notes/components/note_signed_out_widget_spec.js
+++ b/spec/javascripts/notes/components/note_signed_out_widget_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import noteSignedOut from '~/notes/components/note_signed_out_widget.vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import { notesDataMock } from '../mock_data';
describe('note_signed_out_widget component', () => {
+ let store;
let vm;
beforeEach(() => {
const Component = Vue.extend(noteSignedOut);
+ store = createStore();
store.dispatch('setNotesData', notesDataMock);
vm = new Component({
@@ -20,18 +22,20 @@ describe('note_signed_out_widget component', () => {
});
it('should render sign in link provided in the store', () => {
- expect(
- vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent,
- ).toEqual('sign in');
+ expect(vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent).toEqual(
+ 'sign in',
+ );
});
it('should render register link provided in the store', () => {
- expect(
- vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent,
- ).toEqual('register');
+ expect(vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent).toEqual(
+ 'register',
+ );
});
it('should render information text', () => {
- expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply');
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual(
+ 'Please register or sign in to reply',
+ );
});
});
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index cda550760fe..058ddb6202f 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -1,21 +1,24 @@
import Vue from 'vue';
-import store from '~/notes/stores';
-import issueDiscussion from '~/notes/components/noteable_discussion.vue';
+import createStore from '~/notes/stores';
+import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
+import '~/behaviors/markdown/render_gfm';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
-describe('issue_discussion component', () => {
+describe('noteable_discussion component', () => {
+ let store;
let vm;
beforeEach(() => {
- const Component = Vue.extend(issueDiscussion);
+ const Component = Vue.extend(noteableDiscussion);
+ store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
vm = new Component({
store,
propsData: {
- note: discussionMock,
+ discussion: discussionMock,
},
}).$mount();
});
@@ -55,4 +58,74 @@ describe('issue_discussion component', () => {
).toBeNull();
});
});
+
+ describe('computed', () => {
+ describe('hasMultipleUnresolvedDiscussions', () => {
+ it('is false if there are no unresolved discussions', done => {
+ spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([]);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.hasMultipleUnresolvedDiscussions).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('is false if there is one unresolved discussion', done => {
+ spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([discussionMock]);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.hasMultipleUnresolvedDiscussions).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('is true if there are two unresolved discussions', done => {
+ spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([{}, {}]);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.hasMultipleUnresolvedDiscussions).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('jumpToNextDiscussion', () => {
+ it('expands next unresolved discussion', () => {
+ spyOn(vm, 'expandDiscussion').and.stub();
+ const discussions = [
+ discussionMock,
+ {
+ ...discussionMock,
+ id: discussionMock.id + 1,
+ notes: [{ ...discussionMock.notes[0], resolved: true }],
+ },
+ {
+ ...discussionMock,
+ id: discussionMock.id + 2,
+ notes: [{ ...discussionMock.notes[0], resolved: false }],
+ },
+ ];
+ const nextDiscussionId = discussionMock.id + 2;
+ store.replaceState({
+ ...store.state,
+ discussions,
+ });
+ setFixtures(`
+ <div data-discussion-id="${nextDiscussionId}"></div>
+ `);
+
+ vm.jumpToNextDiscussion();
+
+ expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: nextDiscussionId });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js
index 2ffdec7314d..a31d17cacbb 100644
--- a/spec/javascripts/notes/components/noteable_note_spec.js
+++ b/spec/javascripts/notes/components/noteable_note_spec.js
@@ -1,16 +1,18 @@
import $ from 'jquery';
import _ from 'underscore';
import Vue from 'vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import issueNote from '~/notes/components/noteable_note.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note', () => {
+ let store;
let vm;
beforeEach(() => {
const Component = Vue.extend(issueNote);
+ store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
@@ -27,11 +29,14 @@ describe('issue_note', () => {
});
it('should render user information', () => {
- expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(note.author.avatar_url);
+ expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(
+ note.author.avatar_url,
+ );
});
it('should render note header content', () => {
- expect(vm.$el.querySelector('.note-header .note-header-author-name').textContent.trim()).toEqual(note.author.name);
+ const el = vm.$el.querySelector('.note-header .note-header-author-name');
+ expect(el.textContent.trim()).toEqual(note.author.name);
});
it('should render note actions', () => {
@@ -42,7 +47,7 @@ describe('issue_note', () => {
expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
});
- it('prevents note preview xss', (done) => {
+ it('prevents note preview xss', done => {
const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`;
const alertSpy = spyOn(window, 'alert');
@@ -58,7 +63,7 @@ describe('issue_note', () => {
});
describe('cancel edit', () => {
- it('restores content of updated note', (done) => {
+ it('restores content of updated note', done => {
const noteBody = 'updated note text';
vm.updateNote = () => Promise.resolve();
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index fa7adc32193..547efa32694 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -51,6 +51,7 @@ export const noteableDataMock = {
time_estimate: 0,
title: '14',
total_time_spent: 0,
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
updated_at: '2017-08-04T09:53:01.226Z',
updated_by_id: 1,
web_url: '/gitlab-org/gitlab-ce/issues/26',
@@ -99,6 +100,8 @@ export const individualNote = {
{ name: 'art', user: { id: 1, name: 'Root', username: 'root' } },
],
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ note_url: '/group/project/merge_requests/1#note_1',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1',
path: '/gitlab-org/gitlab-ce/notes/1390',
@@ -157,6 +160,8 @@ export const note = {
},
],
toggle_award_path: '/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji',
+ note_url: '/group/project/merge_requests/1#note_1',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1',
path: '/gitlab-org/gitlab-ce/notes/546',
@@ -198,6 +203,7 @@ export const discussionMock = {
discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
emoji_awardable: true,
award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1',
@@ -244,6 +250,7 @@ export const discussionMock = {
emoji_awardable: true,
award_emoji: [],
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1',
path: '/gitlab-org/gitlab-ce/notes/1396',
@@ -288,6 +295,7 @@ export const discussionMock = {
discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
emoji_awardable: true,
award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1',
@@ -335,6 +343,7 @@ export const loggedOutnoteableData = {
can_create_note: false,
can_update: false,
},
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue',
preview_note_path:
'/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
@@ -469,6 +478,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
},
},
],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1',
@@ -513,6 +523,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
emoji_awardable: true,
award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1',
@@ -567,6 +578,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
emoji_awardable: true,
award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
@@ -618,6 +630,7 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = {
emoji_awardable: true,
award_emoji: [],
toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
path: '/gitlab-org/gitlab-ce/notes/1471',
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 520a25cc5c6..985c2f81ef3 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import _ from 'underscore';
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import * as actions from '~/notes/stores/actions';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import testAction from '../../helpers/vuex_action_helper';
import { resetStore } from '../helpers';
import {
@@ -14,6 +14,12 @@ import {
} from '../mock_data';
describe('Actions Notes Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
afterEach(() => {
resetStore(store);
});
@@ -76,7 +82,7 @@ describe('Actions Notes Store', () => {
actions.setInitialNotes,
[individualNote],
{ notes: [] },
- [{ type: 'SET_INITIAL_NOTES', payload: [individualNote] }],
+ [{ type: 'SET_INITIAL_DISCUSSIONS', payload: [individualNote] }],
[],
done,
);
@@ -109,6 +115,19 @@ describe('Actions Notes Store', () => {
});
});
+ describe('expandDiscussion', () => {
+ it('should expand discussion', done => {
+ testAction(
+ actions.expandDiscussion,
+ { discussionId: discussionMock.id },
+ { notes: [discussionMock] },
+ [{ type: 'EXPAND_DISCUSSION', payload: { discussionId: discussionMock.id } }],
+ [],
+ done,
+ );
+ });
+ });
+
describe('async methods', () => {
const interceptor = (request, next) => {
next(
@@ -194,7 +213,14 @@ describe('Actions Notes Store', () => {
});
it('sets issue state as reopened', done => {
- testAction(actions.toggleIssueLocalState, 'reopened', {}, [{ type: 'REOPEN_ISSUE' }], [], done);
+ testAction(
+ actions.toggleIssueLocalState,
+ 'reopened',
+ {},
+ [{ type: 'REOPEN_ISSUE' }],
+ [],
+ done,
+ );
});
});
@@ -239,13 +265,7 @@ describe('Actions Notes Store', () => {
.dispatch('poll')
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
.then(() => {
- expect(Vue.http.get).toHaveBeenCalledWith(jasmine.anything(), {
- url: jasmine.anything(),
- method: 'get',
- headers: {
- 'X-Last-Fetched-At': undefined,
- },
- });
+ expect(Vue.http.get).toHaveBeenCalled();
expect(store.state.lastFetchedAt).toBe('123456');
jasmine.clock().tick(1500);
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index e5550580bf8..5501e50e97b 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -1,12 +1,18 @@
import * as getters from '~/notes/stores/getters';
-import { notesDataMock, userDataMock, noteableDataMock, individualNote, collapseNotesMock } from '../mock_data';
+import {
+ notesDataMock,
+ userDataMock,
+ noteableDataMock,
+ individualNote,
+ collapseNotesMock,
+} from '../mock_data';
describe('Getters Notes Store', () => {
let state;
beforeEach(() => {
state = {
- notes: [individualNote],
+ discussions: [individualNote],
targetNoteHash: 'hash',
lastFetchedAt: 'timestamp',
@@ -15,15 +21,15 @@ describe('Getters Notes Store', () => {
noteableData: noteableDataMock,
};
});
- describe('notes', () => {
- it('should return all notes in the store', () => {
- expect(getters.notes(state)).toEqual([individualNote]);
+ describe('discussions', () => {
+ it('should return all discussions in the store', () => {
+ expect(getters.discussions(state)).toEqual([individualNote]);
});
});
describe('Collapsed notes', () => {
const stateCollapsedNotes = {
- notes: collapseNotesMock,
+ discussions: collapseNotesMock,
targetNoteHash: 'hash',
lastFetchedAt: 'timestamp',
@@ -33,7 +39,7 @@ describe('Getters Notes Store', () => {
};
it('should return a single system note when a description was updated multiple times', () => {
- expect(getters.notes(stateCollapsedNotes).length).toEqual(1);
+ expect(getters.discussions(stateCollapsedNotes).length).toEqual(1);
});
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 98f101d6bc5..556a1c244c0 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -1,5 +1,12 @@
import mutations from '~/notes/stores/mutations';
-import { note, discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
+import {
+ note,
+ discussionMock,
+ notesDataMock,
+ userDataMock,
+ noteableDataMock,
+ individualNote,
+} from '../mock_data';
describe('Notes Store mutations', () => {
describe('ADD_NEW_NOTE', () => {
@@ -7,7 +14,7 @@ describe('Notes Store mutations', () => {
let noteData;
beforeEach(() => {
- state = { notes: [] };
+ state = { discussions: [] };
noteData = {
expanded: true,
id: note.discussion_id,
@@ -20,46 +27,60 @@ describe('Notes Store mutations', () => {
it('should add a new note to an array of notes', () => {
expect(state).toEqual({
- notes: [noteData],
+ discussions: [noteData],
});
- expect(state.notes.length).toBe(1);
+ expect(state.discussions.length).toBe(1);
});
it('should not add the same note to the notes array', () => {
mutations.ADD_NEW_NOTE(state, note);
- expect(state.notes.length).toBe(1);
+ expect(state.discussions.length).toBe(1);
});
});
describe('ADD_NEW_REPLY_TO_DISCUSSION', () => {
it('should add a reply to a specific discussion', () => {
- const state = { notes: [discussionMock] };
+ const state = { discussions: [discussionMock] };
const newReply = Object.assign({}, note, { discussion_id: discussionMock.id });
mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply);
- expect(state.notes[0].notes.length).toEqual(4);
+ expect(state.discussions[0].notes.length).toEqual(4);
});
});
describe('DELETE_NOTE', () => {
it('should delete a note ', () => {
- const state = { notes: [discussionMock] };
+ const state = { discussions: [discussionMock] };
const toDelete = discussionMock.notes[0];
const lengthBefore = discussionMock.notes.length;
mutations.DELETE_NOTE(state, toDelete);
- expect(state.notes[0].notes.length).toEqual(lengthBefore - 1);
+ expect(state.discussions[0].notes.length).toEqual(lengthBefore - 1);
+ });
+ });
+
+ describe('EXPAND_DISCUSSION', () => {
+ it('should expand a collapsed discussion', () => {
+ const discussion = Object.assign({}, discussionMock, { expanded: false });
+
+ const state = {
+ discussions: [discussion],
+ };
+
+ mutations.EXPAND_DISCUSSION(state, { discussionId: discussion.id });
+
+ expect(state.discussions[0].expanded).toEqual(true);
});
});
describe('REMOVE_PLACEHOLDER_NOTES', () => {
it('should remove all placeholder notes in indivudal notes and discussion', () => {
const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true });
- const state = { notes: [placeholderNote] };
+ const state = { discussions: [placeholderNote] };
mutations.REMOVE_PLACEHOLDER_NOTES(state);
- expect(state.notes).toEqual([]);
+ expect(state.discussions).toEqual([]);
});
});
@@ -96,26 +117,29 @@ describe('Notes Store mutations', () => {
});
});
- describe('SET_INITIAL_NOTES', () => {
+ describe('SET_INITIAL_DISCUSSIONS', () => {
it('should set the initial notes received', () => {
const state = {
- notes: [],
+ discussions: [],
};
const legacyNote = {
id: 2,
individual_note: true,
- notes: [{
- note: '1',
- }, {
- note: '2',
- }],
+ notes: [
+ {
+ note: '1',
+ },
+ {
+ note: '2',
+ },
+ ],
};
- mutations.SET_INITIAL_NOTES(state, [note, legacyNote]);
- expect(state.notes[0].id).toEqual(note.id);
- expect(state.notes[1].notes[0].note).toBe(legacyNote.notes[0].note);
- expect(state.notes[2].notes[0].note).toBe(legacyNote.notes[1].note);
- expect(state.notes.length).toEqual(3);
+ mutations.SET_INITIAL_DISCUSSIONS(state, [note, legacyNote]);
+ expect(state.discussions[0].id).toEqual(note.id);
+ expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note);
+ expect(state.discussions[2].notes[0].note).toBe(legacyNote.notes[1].note);
+ expect(state.discussions.length).toEqual(3);
});
});
@@ -144,17 +168,17 @@ describe('Notes Store mutations', () => {
describe('SHOW_PLACEHOLDER_NOTE', () => {
it('should set a placeholder note', () => {
const state = {
- notes: [],
+ discussions: [],
};
mutations.SHOW_PLACEHOLDER_NOTE(state, note);
- expect(state.notes[0].isPlaceholderNote).toEqual(true);
+ expect(state.discussions[0].isPlaceholderNote).toEqual(true);
});
});
describe('TOGGLE_AWARD', () => {
it('should add award if user has not reacted yet', () => {
const state = {
- notes: [note],
+ discussions: [note],
userData: userDataMock,
};
@@ -164,9 +188,9 @@ describe('Notes Store mutations', () => {
};
mutations.TOGGLE_AWARD(state, data);
- const lastIndex = state.notes[0].award_emoji.length - 1;
+ const lastIndex = state.discussions[0].award_emoji.length - 1;
- expect(state.notes[0].award_emoji[lastIndex]).toEqual({
+ expect(state.discussions[0].award_emoji[lastIndex]).toEqual({
name: 'cartwheel',
user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username },
});
@@ -174,7 +198,7 @@ describe('Notes Store mutations', () => {
it('should remove award if user already reacted', () => {
const state = {
- notes: [note],
+ discussions: [note],
userData: {
id: 1,
name: 'Administrator',
@@ -187,7 +211,7 @@ describe('Notes Store mutations', () => {
awardName: 'bath_tone3',
};
mutations.TOGGLE_AWARD(state, data);
- expect(state.notes[0].award_emoji.length).toEqual(2);
+ expect(state.discussions[0].award_emoji.length).toEqual(2);
});
});
@@ -196,43 +220,43 @@ describe('Notes Store mutations', () => {
const discussion = Object.assign({}, discussionMock, { expanded: false });
const state = {
- notes: [discussion],
+ discussions: [discussion],
};
mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id });
- expect(state.notes[0].expanded).toEqual(true);
+ expect(state.discussions[0].expanded).toEqual(true);
});
it('should close a opened discussion', () => {
const state = {
- notes: [discussionMock],
+ discussions: [discussionMock],
};
mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id });
- expect(state.notes[0].expanded).toEqual(false);
+ expect(state.discussions[0].expanded).toEqual(false);
});
});
describe('UPDATE_NOTE', () => {
it('should update a note', () => {
const state = {
- notes: [individualNote],
+ discussions: [individualNote],
};
const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' });
mutations.UPDATE_NOTE(state, updated);
- expect(state.notes[0].notes[0].note).toEqual('Foo');
+ expect(state.discussions[0].notes[0].note).toEqual('Foo');
});
});
describe('CLOSE_ISSUE', () => {
it('should set issue as closed', () => {
const state = {
- notes: [],
+ discussions: [],
targetNoteHash: null,
lastFetchedAt: null,
isToggleStateButtonLoading: false,
@@ -249,7 +273,7 @@ describe('Notes Store mutations', () => {
describe('REOPEN_ISSUE', () => {
it('should set issue as closed', () => {
const state = {
- notes: [],
+ discussions: [],
targetNoteHash: null,
lastFetchedAt: null,
isToggleStateButtonLoading: false,
@@ -266,7 +290,7 @@ describe('Notes Store mutations', () => {
describe('TOGGLE_STATE_BUTTON_LOADING', () => {
it('should set isToggleStateButtonLoading as true', () => {
const state = {
- notes: [],
+ discussions: [],
targetNoteHash: null,
lastFetchedAt: null,
isToggleStateButtonLoading: false,
@@ -281,7 +305,7 @@ describe('Notes Store mutations', () => {
it('should set isToggleStateButtonLoading as false', () => {
const state = {
- notes: [],
+ discussions: [],
targetNoteHash: null,
lastFetchedAt: null,
isToggleStateButtonLoading: true,
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 2854263a25a..faeedae40e9 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -35,11 +35,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('Notes', function() {
const FLASH_TYPE_ALERT = 'alert';
const NOTES_POST_PATH = /(.*)\/notes\?html=true$/;
- var commentsTemplate = 'merge_requests/merge_request_with_comment.html.raw';
- preloadFixtures(commentsTemplate);
+ var fixture = 'snippets/show.html.raw';
+ preloadFixtures(fixture);
beforeEach(function() {
- loadFixtures(commentsTemplate);
+ loadFixtures(fixture);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
$('body').attr('data-page', 'projects:merge_requets:show');
@@ -65,16 +65,9 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
let mock;
beforeEach(function() {
- spyOn(axios, 'patch').and.callThrough();
+ spyOn(axios, 'patch').and.callFake(() => new Promise(() => {}));
mock = new MockAdapter(axios);
-
- mock
- .onPatch(
- `${
- gl.TEST_HOST
- }/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
- )
- .reply(200, {});
+ mock.onAny().reply(200, {});
$('.js-comment-button').on('click', function(e) {
e.preventDefault();
@@ -90,26 +83,17 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
$('input[type=checkbox]')
- .attr('checked', true)[1]
+ .attr('checked', true)[0]
.dispatchEvent(changeEvent);
- expect($('.js-task-list-field.original-task-list').val()).toBe(
- '- [x] Task List Item',
- );
+ expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item');
});
it('submits an ajax request on tasklist:changed', function(done) {
$('.js-task-list-container').trigger('tasklist:changed');
setTimeout(() => {
- expect(axios.patch).toHaveBeenCalledWith(
- `${
- gl.TEST_HOST
- }/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
- {
- note: { note: '' },
- },
- );
+ expect(axios.patch).toHaveBeenCalled();
done();
});
});
@@ -200,9 +184,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
updatedNote.note = 'bar';
this.notes.updateNote(updatedNote, $targetNote);
- expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith(
- $targetNote,
- );
+ expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
expect(this.notes.setupNewNote).toHaveBeenCalled();
done();
@@ -282,10 +264,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
Notes.isNewNote.and.returnValue(true);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(
- note.html,
- $notesList,
- );
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
});
});
@@ -300,10 +279,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(Notes.animateUpdateNote).toHaveBeenCalledWith(
- note.html,
- $note,
- );
+ expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note);
expect(notes.setupNewNote).toHaveBeenCalledWith($newNote);
});
@@ -331,10 +307,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$notesList.find.and.returnValue($note);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(
- note,
- $note,
- );
+ expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note);
});
});
});
@@ -400,10 +373,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$form.length = 1;
row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']);
- notes = jasmine.createSpyObj('notes', [
- 'isParallelView',
- 'updateNotesCount',
- ]);
+ notes = jasmine.createSpyObj('notes', ['isParallelView', 'updateNotesCount']);
notes.note_ids = [];
spyOn(Notes, 'isNewNote');
@@ -464,10 +434,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('should call Notes.animateAppendNote', () => {
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(
- note.html,
- discussionContainer,
- );
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer);
});
});
});
@@ -571,9 +538,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
mockNotesPost();
$('.js-comment-button').click();
- expect($notesContainer.find('.note.being-posted').length > 0).toEqual(
- true,
- );
+ expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true);
});
it('should remove placeholder note when new comment is done posting', done => {
@@ -617,9 +582,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$('.js-comment-button').click();
setTimeout(() => {
- expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(
- true,
- );
+ expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true);
done();
});
@@ -734,14 +697,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough();
$('.js-comment-button').click();
- expect(
- $notesContainer.find('.system-note.being-posted').length,
- ).toEqual(1); // Placeholder shown
+ expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
setTimeout(() => {
- expect(
- $notesContainer.find('.system-note.being-posted').length,
- ).toEqual(0); // Placeholder removed
+ expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
done();
});
});
@@ -815,9 +774,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should return form metadata object from form reference', () => {
$form.find('textarea.js-note-text').val(sampleComment);
- const { formData, formContent, formAction } = this.notes.getFormData(
- $form,
- );
+ const { formData, formContent, formAction } = this.notes.getFormData($form);
expect(formData.indexOf(sampleComment) > -1).toBe(true);
expect(formContent).toEqual(sampleComment);
@@ -833,9 +790,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const { formContent } = this.notes.getFormData($form);
expect(_.escape).toHaveBeenCalledWith(sampleComment);
- expect(formContent).toEqual(
- '&lt;script&gt;alert(&quot;Boom!&quot;);&lt;/script&gt;',
- );
+ expect(formContent).toEqual('&lt;script&gt;alert(&quot;Boom!&quot;);&lt;/script&gt;');
});
});
@@ -845,8 +800,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('should return true when comment begins with a quick action', () => {
- const sampleComment =
- '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
+ const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const hasQuickActions = this.notes.hasQuickActions(sampleComment);
expect(hasQuickActions).toBeTruthy();
@@ -870,8 +824,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('stripQuickActions', () => {
it('should strip quick actions from the comment which begins with a quick action', () => {
this.notes = new Notes();
- const sampleComment =
- '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
+ const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('');
@@ -879,8 +832,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should strip quick actions from the comment but leaves plain comment if it is present', () => {
this.notes = new Notes();
- const sampleComment =
- '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
+ const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('Merging this');
@@ -888,8 +840,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should NOT strip string that has slashes within', () => {
this.notes = new Notes();
- const sampleComment =
- 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
+ const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe(sampleComment);
@@ -909,29 +860,21 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should return executing quick action description when note has single quick action', () => {
const sampleComment = '/close';
- expect(
- this.notes.getQuickActionDescription(
- sampleComment,
- availableQuickActions,
- ),
- ).toBe('Applying command to close this issue');
+ expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
+ 'Applying command to close this issue',
+ );
});
it('should return generic multiple quick action description when note has multiple quick actions', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(
- this.notes.getQuickActionDescription(
- sampleComment,
- availableQuickActions,
- ),
- ).toBe('Applying multiple commands');
+ expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
+ 'Applying multiple commands',
+ );
});
it('should return generic quick action description when available quick actions list is not populated', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(this.notes.getQuickActionDescription(sampleComment)).toBe(
- 'Applying command',
- );
+ expect(this.notes.getQuickActionDescription(sampleComment)).toBe('Applying command');
});
});
@@ -961,17 +904,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
expect($tempNote.attr('id')).toEqual(uniqueId);
expect($tempNote.hasClass('being-posted')).toBeTruthy();
expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
- $tempNote
- .find('.timeline-icon > a, .note-header-info > a')
- .each(function() {
- expect($(this).attr('href')).toEqual(`/${currentUsername}`);
- });
- expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(
- currentUserAvatar,
- );
- expect(
- $tempNote.find('.timeline-content').hasClass('discussion'),
- ).toBeFalsy();
+ $tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() {
+ expect($(this).attr('href')).toEqual(`/${currentUsername}`);
+ });
+ expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar);
+ expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
expect(
$tempNoteHeader
.find('.d-none.d-sm-inline-block')
@@ -1002,9 +939,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
expect($tempNote.prop('nodeName')).toEqual('LI');
- expect(
- $tempNote.find('.timeline-content').hasClass('discussion'),
- ).toBeTruthy();
+ expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy();
});
it('should return a escaped user name', () => {
@@ -1061,11 +996,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('shows a flash message', () => {
- this.notes.addFlash(
- 'Error message',
- FLASH_TYPE_ALERT,
- this.notes.parentTimeline.get(0),
- );
+ this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
expect($('.flash-alert').is(':visible')).toBeTruthy();
});
@@ -1078,11 +1009,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('hides visible flash message', () => {
- this.notes.addFlash(
- 'Error message 1',
- FLASH_TYPE_ALERT,
- this.notes.parentTimeline.get(0),
- );
+ this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
this.notes.clearFlash();
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index 68043b91bd0..03ffc122795 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -4,7 +4,7 @@ import eventHub from '~/pipelines/event_hub';
describe('Pipelines Table Row', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
- const buildComponent = (pipeline) => {
+ const buildComponent = pipeline => {
const PipelinesTableRowComponent = Vue.extend(tableRowComp);
return new PipelinesTableRowComponent({
el: document.querySelector('.test-dom-element'),
@@ -24,7 +24,7 @@ describe('Pipelines Table Row', () => {
preloadFixtures(jsonFixtureName);
beforeEach(() => {
- const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ const { pipelines } = getJSONFixture(jsonFixtureName);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
pipelineWithoutAuthor = pipelines.find(p => p.user === null && p.commit !== null);
@@ -52,9 +52,9 @@ describe('Pipelines Table Row', () => {
});
it('should render status text', () => {
- expect(
- component.$el.querySelector('.table-section.commit-link a').textContent,
- ).toContain(pipeline.details.status.text);
+ expect(component.$el.querySelector('.table-section.commit-link a').textContent).toContain(
+ pipeline.details.status.text,
+ );
});
});
@@ -78,11 +78,15 @@ describe('Pipelines Table Row', () => {
describe('when a user is provided', () => {
it('should render user information', () => {
expect(
- component.$el.querySelector('.table-section:nth-child(2) a:nth-child(3)').getAttribute('href'),
+ component.$el
+ .querySelector('.table-section:nth-child(2) a:nth-child(3)')
+ .getAttribute('href'),
).toEqual(pipeline.user.path);
expect(
- component.$el.querySelector('.table-section:nth-child(2) img').getAttribute('data-original-title'),
+ component.$el
+ .querySelector('.table-section:nth-child(2) img')
+ .getAttribute('data-original-title'),
).toEqual(pipeline.user.name);
});
});
@@ -105,7 +109,9 @@ describe('Pipelines Table Row', () => {
}
const commitAuthorLink = commitAuthorElement.getAttribute('href');
- const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('data-original-title');
+ const commitAuthorName = commitAuthorElement
+ .querySelector('img.avatar')
+ .getAttribute('data-original-title');
return { commitAuthorElement, commitAuthorLink, commitAuthorName };
};
@@ -145,7 +151,8 @@ describe('Pipelines Table Row', () => {
it('should render an icon for each stage', () => {
expect(
- component.$el.querySelectorAll('.table-section:nth-child(4) .js-builds-dropdown-button').length,
+ component.$el.querySelectorAll('.table-section:nth-child(4) .js-builds-dropdown-button')
+ .length,
).toEqual(pipeline.details.stages.length);
});
});
@@ -167,7 +174,7 @@ describe('Pipelines Table Row', () => {
});
it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => {
- eventHub.$on('retryPipeline', (endpoint) => {
+ eventHub.$on('retryPipeline', endpoint => {
expect(endpoint).toEqual('/retry');
});
@@ -176,7 +183,7 @@ describe('Pipelines Table Row', () => {
});
it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => {
- eventHub.$on('openConfirmationModal', (data) => {
+ eventHub.$once('openConfirmationModal', data => {
expect(data.endpoint).toEqual('/cancel');
expect(data.pipelineId).toEqual(pipeline.id);
});
diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js
index 4fc3c08145e..d21ba35e96d 100644
--- a/spec/javascripts/pipelines/pipelines_table_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_spec.js
@@ -11,7 +11,7 @@ describe('Pipelines Table', () => {
preloadFixtures(jsonFixtureName);
beforeEach(() => {
- const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ const { pipelines } = getJSONFixture(jsonFixtureName);
PipelinesTableComponent = Vue.extend(pipelinesTableComp);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index b10d8be6781..a4753ab7cde 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -4,71 +4,102 @@ import ShortcutsIssuable from '~/shortcuts_issuable';
initCopyAsGFM();
-describe('ShortcutsIssuable', function () {
- const fixtureName = 'merge_requests/diff_comment.html.raw';
+const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
+
+describe('ShortcutsIssuable', function() {
+ const fixtureName = 'snippets/show.html.raw';
preloadFixtures(fixtureName);
+
beforeEach(() => {
loadFixtures(fixtureName);
+ $('body').append(
+ `<div class="js-main-target-form">
+ <textare class="js-vue-comment-form"></textare>
+ </div>`,
+ );
document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
this.shortcut = new ShortcutsIssuable(true);
});
+
+ afterEach(() => {
+ $(FORM_SELECTOR).remove();
+ });
+
describe('replyWithSelectedText', () => {
// Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML.
- const stubSelection = (html) => {
+ const stubSelection = html => {
window.gl.utils.getSelectedFragment = () => {
const node = document.createElement('div');
node.innerHTML = html;
+
return node;
};
};
- beforeEach(() => {
- this.selector = '.js-main-target-form #note_note';
- });
describe('with empty selection', () => {
it('does not return an error', () => {
- this.shortcut.replyWithSelectedText(true);
- expect($(this.selector).val()).toBe('');
+ ShortcutsIssuable.replyWithSelectedText(true);
+
+ expect($(FORM_SELECTOR).val()).toBe('');
});
+
it('triggers `focus`', () => {
- this.shortcut.replyWithSelectedText(true);
- expect(document.activeElement).toBe(document.querySelector(this.selector));
+ const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
+ ShortcutsIssuable.replyWithSelectedText(true);
+
+ expect(spy).toHaveBeenCalled();
});
});
+
describe('with any selection', () => {
beforeEach(() => {
stubSelection('<p>Selected text.</p>');
});
+
it('leaves existing input intact', () => {
- $(this.selector).val('This text was already here.');
- expect($(this.selector).val()).toBe('This text was already here.');
- this.shortcut.replyWithSelectedText(true);
- expect($(this.selector).val()).toBe('This text was already here.\n\n> Selected text.\n\n');
+ $(FORM_SELECTOR).val('This text was already here.');
+ expect($(FORM_SELECTOR).val()).toBe('This text was already here.');
+
+ ShortcutsIssuable.replyWithSelectedText(true);
+ expect($(FORM_SELECTOR).val()).toBe('This text was already here.\n\n> Selected text.\n\n');
});
+
it('triggers `input`', () => {
let triggered = false;
- $(this.selector).on('input', () => {
+ $(FORM_SELECTOR).on('input', () => {
triggered = true;
});
- this.shortcut.replyWithSelectedText(true);
+
+ ShortcutsIssuable.replyWithSelectedText(true);
expect(triggered).toBe(true);
});
+
it('triggers `focus`', () => {
- this.shortcut.replyWithSelectedText(true);
- expect(document.activeElement).toBe(document.querySelector(this.selector));
+ const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
+ ShortcutsIssuable.replyWithSelectedText(true);
+
+ expect(spy).toHaveBeenCalled();
});
});
+
describe('with a one-line selection', () => {
it('quotes the selection', () => {
stubSelection('<p>This text has been selected.</p>');
- this.shortcut.replyWithSelectedText(true);
- expect($(this.selector).val()).toBe('> This text has been selected.\n\n');
+ ShortcutsIssuable.replyWithSelectedText(true);
+
+ expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n');
});
});
+
describe('with a multi-line selection', () => {
it('quotes the selected lines as a group', () => {
- stubSelection('<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>');
- this.shortcut.replyWithSelectedText(true);
- expect($(this.selector).val()).toBe('> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n');
+ stubSelection(
+ '<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>',
+ );
+ ShortcutsIssuable.replyWithSelectedText(true);
+
+ expect($(FORM_SELECTOR).val()).toBe(
+ '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n',
+ );
});
});
});
diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js
index ee92295ef5e..94cded7ee37 100644
--- a/spec/javascripts/shortcuts_spec.js
+++ b/spec/javascripts/shortcuts_spec.js
@@ -2,10 +2,11 @@ import $ from 'jquery';
import Shortcuts from '~/shortcuts';
describe('Shortcuts', () => {
- const fixtureName = 'merge_requests/diff_comment.html.raw';
- const createEvent = (type, target) => $.Event(type, {
- target,
- });
+ const fixtureName = 'snippets/show.html.raw';
+ const createEvent = (type, target) =>
+ $.Event(type, {
+ target,
+ });
preloadFixtures(fixtureName);
@@ -21,19 +22,19 @@ describe('Shortcuts', () => {
it('focuses preview button in form', () => {
Shortcuts.toggleMarkdownPreview(
- createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text'),
- ));
+ createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text')),
+ );
expect('focus').toHaveBeenTriggeredOn('.js-new-note-form .js-md-preview-button');
});
- it('focues preview button inside edit comment form', (done) => {
+ it('focues preview button inside edit comment form', done => {
document.querySelector('.js-note-edit').click();
setTimeout(() => {
Shortcuts.toggleMarkdownPreview(
- createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text'),
- ));
+ createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text')),
+ );
expect('focus').not.toHaveBeenTriggeredOn('.js-new-note-form .js-md-preview-button');
expect('focus').toHaveBeenTriggeredOn('.edit-note .js-md-preview-button');
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js
index 423432c9e5d..9d3905fa1d8 100644
--- a/spec/javascripts/signin_tabs_memoizer_spec.js
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js
@@ -45,6 +45,21 @@ import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
expect(fakeTab.click).toHaveBeenCalled();
});
+ it('clicks the first tab if value in local storage is bad', () => {
+ createMemoizer().saveData('#bogus');
+ const fakeTab = {
+ click: () => {},
+ };
+ spyOn(document, 'querySelector').and.callFake(selector => (selector === `${tabSelector} a[href="#bogus"]` ? null : fakeTab));
+ spyOn(fakeTab, 'click');
+
+ memo.bootstrap();
+
+ // verify that triggers click on stored selector and fallback
+ expect(document.querySelector.calls.allArgs()).toEqual([['ul.new-session-tabs a[href="#bogus"]'], ['ul.new-session-tabs a']]);
+ expect(fakeTab.click).toHaveBeenCalled();
+ });
+
it('saves last selected tab on change', () => {
createMemoizer();
diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js
index a54219d58c2..60153672214 100644
--- a/spec/javascripts/smart_interval_spec.js
+++ b/spec/javascripts/smart_interval_spec.js
@@ -87,7 +87,7 @@ describe('SmartInterval', function () {
setTimeout(() => {
interval.cancel();
- const intervalId = interval.state.intervalId;
+ const { intervalId } = interval.state;
const currentInterval = interval.getCurrentInterval();
const intervalLowerLimit = interval.cfg.startingInterval;
@@ -106,7 +106,7 @@ describe('SmartInterval', function () {
interval.resume();
- const intervalId = interval.state.intervalId;
+ const { intervalId } = interval.state;
expect(intervalId).toBeTruthy();
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 994011b262a..0eff98bcc9d 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -3,7 +3,6 @@
import $ from 'jquery';
import 'vendor/jasmine-jquery';
import '~/commons';
-
import Vue from 'vue';
import VueResource from 'vue-resource';
import Translate from '~/vue_shared/translate';
@@ -91,7 +90,8 @@ testsContext.keys().forEach(function(path) {
try {
testsContext(path);
} catch (err) {
- console.error('[ERROR] Unable to load spec: ', path);
+ console.log(err);
+ console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path);
describe('Test bundle', function() {
it(`includes '${path}'`, function() {
expect(err).toBeNull();
@@ -135,7 +135,7 @@ if (process.env.BABEL_ENV === 'coverage') {
// exempt these files from the coverage report
const troubleMakers = [
'./blob_edit/blob_bundle.js',
- './boards/components/modal/empty_state.js',
+ './boards/components/modal/empty_state.vue',
'./boards/components/modal/footer.js',
'./boards/components/modal/header.js',
'./cycle_analytics/cycle_analytics_bundle.js',
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index d84b13b07c4..57e0caa692c 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -6,7 +6,7 @@ import MockU2FDevice from './mock_u2f_device';
describe('U2FAuthenticate', function () {
preloadFixtures('u2f/authenticate.html.raw');
- beforeEach((done) => {
+ beforeEach(() => {
loadFixtures('u2f/authenticate.html.raw');
this.u2fDevice = new MockU2FDevice();
this.container = $('#js-authenticate-u2f');
@@ -19,46 +19,70 @@ describe('U2FAuthenticate', function () {
document.querySelector('#js-login-2fa-device'),
document.querySelector('.js-2fa-form'),
);
+ });
- // bypass automatic form submission within renderAuthenticated
- spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
+ describe('with u2f unavailable', () => {
+ beforeEach(() => {
+ spyOn(this.component, 'switchToFallbackUI');
+ this.oldu2f = window.u2f;
+ window.u2f = null;
+ });
- this.component.start().then(done).catch(done.fail);
- });
+ afterEach(() => {
+ window.u2f = this.oldu2f;
+ });
- 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('falls back to normal 2fa', (done) => {
+ this.component.start().then(() => {
+ expect(this.component.switchToFallbackUI).toHaveBeenCalled();
+ done();
+ }).catch(done.fail);
});
- expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
- describe('errors', () => {
- it('displays an error message', () => {
- const setupButton = this.container.find('#js-login-u2f-device');
- setupButton.trigger('click');
- this.u2fDevice.respondToAuthenticateRequest({
- errorCode: 'error!',
- });
- const errorMessage = this.container.find('p');
- return expect(errorMessage.text()).toContain('There was a problem communicating with your device');
+ describe('with u2f available', () => {
+ beforeEach((done) => {
+ // bypass automatic form submission within renderAuthenticated
+ spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
+ this.u2fDevice = new MockU2FDevice();
+
+ this.component.start().then(done).catch(done.fail);
});
- 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!',
- });
- const retryButton = this.container.find('#js-u2f-try-again');
- retryButton.trigger('click');
- setupButton = this.container.find('#js-login-u2f-device');
- setupButton.trigger('click');
+
+ 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',
});
expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
+
+ describe('errors', () => {
+ it('displays an error message', () => {
+ const setupButton = this.container.find('#js-login-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ errorCode: 'error!',
+ });
+ const 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!',
+ });
+ 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"}');
+ });
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js
index f7581251bf0..1c666fc6c55 100644
--- a/spec/javascripts/vue_shared/components/file_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/file_icon_spec.js
@@ -74,7 +74,7 @@ describe('File Icon component', () => {
size: 120,
});
- const classList = vm.$el.firstChild.classList;
+ const { classList } = vm.$el.firstChild;
const containsSizeClass = classList.contains('s120');
const containsCustomClass = classList.contains('extraclasses');
expect(containsSizeClass).toBe(true);
diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js
index 68d57ebc8f0..cc030e29d61 100644
--- a/spec/javascripts/vue_shared/components/icon_spec.js
+++ b/spec/javascripts/vue_shared/components/icon_spec.js
@@ -44,7 +44,7 @@ describe('Sprite Icon Component', function () {
});
it('should properly render img css', function () {
- const classList = icon.$el.classList;
+ const { classList } = icon.$el;
const containsSizeClass = classList.contains('s32');
const containsCustomClass = classList.contains('extraclasses');
expect(containsSizeClass).toBe(true);
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
index ba8ab0b2cd7..7e57c51bf29 100644
--- a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
import { userDataMock } from '../../../notes/mock_data';
describe('issue placeholder system note component', () => {
+ let store;
let vm;
beforeEach(() => {
const Component = Vue.extend(issuePlaceholderNote);
+ store = createStore();
store.dispatch('setUserData', userDataMock);
vm = new Component({
store,
@@ -21,15 +23,23 @@ describe('issue placeholder system note component', () => {
describe('user information', () => {
it('should render user avatar with link', () => {
- expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path);
- expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(userDataMock.avatar_url);
+ expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(
+ userDataMock.path,
+ );
+ expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(
+ userDataMock.avatar_url,
+ );
});
});
describe('note content', () => {
it('should render note header information', () => {
- expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(userDataMock.path);
- expect(vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim()).toEqual(`@${userDataMock.username}`);
+ expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(
+ userDataMock.path,
+ );
+ expect(
+ vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim(),
+ ).toEqual(`@${userDataMock.username}`);
});
it('should render note body', () => {
diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js
index 36aaf0a6c2e..aa4c9c4c88c 100644
--- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js
+++ b/spec/javascripts/vue_shared/components/notes/system_note_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import issueSystemNote from '~/vue_shared/components/notes/system_note.vue';
-import store from '~/notes/stores';
+import createStore from '~/notes/stores';
-describe('issue system note', () => {
+describe('system note component', () => {
let vm;
let props;
@@ -24,6 +24,7 @@ describe('issue system note', () => {
},
};
+ const store = createStore();
store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
const Component = Vue.extend(issueSystemNote);
@@ -49,9 +50,10 @@ describe('issue system note', () => {
expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined();
});
- it('should render note header component', () => {
- expect(
- vm.$el.querySelector('.system-note-message').innerHTML,
- ).toEqual(props.note.note_html);
+ // Redcarpet Markdown renderer wraps text in `<p>` tags
+ // we need to strip them because they break layout of commit lists in system notes:
+ // https://gitlab.com/gitlab-org/gitlab-ce/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
+ it('removes wrapping paragraph from note HTML', () => {
+ expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>');
});
});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
index 446f025c127..656b57d764e 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -51,7 +51,7 @@ describe('User Avatar Image Component', function () {
});
it('should properly render img css', function () {
- const classList = vm.$el.classList;
+ const { classList } = vm.$el;
const containsAvatar = classList.contains('avatar');
const containsSizeClass = classList.contains('s99');
const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses);
@@ -73,7 +73,7 @@ describe('User Avatar Image Component', function () {
});
it('should add lazy attributes', function () {
- const classList = vm.$el.classList;
+ const { classList } = vm.$el;
const lazyClass = classList.contains('lazy');
expect(lazyClass).toBe(true);
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
index adf80d0c2bb..4c5c242cbb3 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -21,7 +21,7 @@ describe('User Avatar Link Component', function () {
propsData: this.propsData,
}).$mount();
- this.userAvatarImage = this.userAvatarLink.$children[0];
+ [this.userAvatarImage] = this.userAvatarLink.$children;
});
it('should return a defined Vue component', function () {
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index 7fe3bd92049..bdeebe0de75 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -1,11 +1,12 @@
import $ from 'jquery';
-import Mousetrap from 'mousetrap';
import Dropzone from 'dropzone';
+import Mousetrap from 'mousetrap';
import ZenMode from '~/zen_mode';
describe('ZenMode', () => {
let zen;
- const fixtureName = 'merge_requests/merge_request_with_comment.html.raw';
+ let dropzoneForElementSpy;
+ const fixtureName = 'snippets/show.html.raw';
preloadFixtures(fixtureName);
@@ -18,15 +19,17 @@ describe('ZenMode', () => {
}
function escapeKeydown() {
- $('.notes-form textarea').trigger($.Event('keydown', {
- keyCode: 27,
- }));
+ $('.notes-form textarea').trigger(
+ $.Event('keydown', {
+ keyCode: 27,
+ }),
+ );
}
beforeEach(() => {
loadFixtures(fixtureName);
- spyOn(Dropzone, 'forElement').and.callFake(() => ({
+ dropzoneForElementSpy = spyOn(Dropzone, 'forElement').and.callFake(() => ({
enable: () => true,
}));
zen = new ZenMode();
@@ -35,11 +38,29 @@ describe('ZenMode', () => {
zen.scroll_position = 456;
});
+ describe('enabling dropzone', () => {
+ beforeEach(() => {
+ enterZen();
+ });
+
+ it('should not call dropzone if element is not dropzone valid', () => {
+ $('.div-dropzone').addClass('js-invalid-dropzone');
+ exitZen();
+ expect(dropzoneForElementSpy.calls.count()).toEqual(0);
+ });
+
+ it('should call dropzone if element is dropzone valid', () => {
+ $('.div-dropzone').removeClass('js-invalid-dropzone');
+ exitZen();
+ expect(dropzoneForElementSpy.calls.count()).toEqual(2);
+ });
+ });
+
describe('on enter', () => {
it('pauses Mousetrap', () => {
- spyOn(Mousetrap, 'pause');
+ const mouseTrapPauseSpy = spyOn(Mousetrap, 'pause');
enterZen();
- expect(Mousetrap.pause).toHaveBeenCalled();
+ expect(mouseTrapPauseSpy).toHaveBeenCalled();
});
it('removes textarea styling', () => {
@@ -62,9 +83,9 @@ describe('ZenMode', () => {
beforeEach(enterZen);
it('unpauses Mousetrap', () => {
- spyOn(Mousetrap, 'unpause');
+ const mouseTrapUnpauseSpy = spyOn(Mousetrap, 'unpause');
exitZen();
- expect(Mousetrap.unpause).toHaveBeenCalled();
+ expect(mouseTrapUnpauseSpy).toHaveBeenCalled();
});
it('restores the scroll position', () => {
@@ -73,22 +94,4 @@ describe('ZenMode', () => {
expect(zen.scrollTo).toHaveBeenCalled();
});
});
-
- describe('enabling dropzone', () => {
- beforeEach(() => {
- enterZen();
- });
-
- it('should not call dropzone if element is not dropzone valid', () => {
- $('.div-dropzone').addClass('js-invalid-dropzone');
- exitZen();
- expect(Dropzone.forElement).not.toHaveBeenCalled();
- });
-
- it('should call dropzone if element is dropzone valid', () => {
- $('.div-dropzone').removeClass('js-invalid-dropzone');
- exitZen();
- expect(Dropzone.forElement).toHaveBeenCalled();
- });
- });
});
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index b30f3661e70..00257ed7904 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -148,9 +148,11 @@ describe Banzai::Filter::LabelReferenceFilter do
expect(doc.text).to eq 'See ?g.fm&'
end
- it 'links with adjacent text' do
- doc = reference_filter("Label (#{reference}).")
- expect(doc.to_html).to match(%r(\(<a.+><span.+>\?g\.fm&amp;</span></a>\)\.))
+ it 'does not include trailing punctuation', :aggregate_failures do
+ ['.', ', ok?', '...', '?', '!', ': is that ok?'].each do |trailing_punctuation|
+ doc = filter("Label #{reference}#{trailing_punctuation}")
+ expect(doc.to_html).to match(%r(<a.+><span.+>\?g\.fm&amp;</span></a>#{Regexp.escape(trailing_punctuation)}))
+ end
end
it 'ignores invalid label names' do
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 17a620ef603..d930c608b18 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -93,6 +93,16 @@ describe Banzai::Filter::SanitizationFilter do
expect(doc.at_css('td')['style']).to eq 'text-align: center'
end
+ it 'disallows `text-align` property in `style` attribute on other elements' do
+ html = <<~HTML
+ <div style="text-align: center">Text</div>
+ HTML
+
+ doc = filter(html)
+
+ expect(doc.at_css('div')['style']).to be_nil
+ end
+
it 'allows `span` elements' do
exp = act = %q{<span>Hello</span>}
expect(filter(act).to_html).to eq exp
@@ -224,7 +234,7 @@ describe Banzai::Filter::SanitizationFilter do
'protocol-based JS injection: spaces and entities' => {
input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
- output: '<a href="">foo</a>'
+ output: '<a href>foo</a>'
},
'protocol whitespace' => {
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 0cfef4ff5bf..7213cd58ea7 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -139,5 +139,14 @@ describe Banzai::Filter::TableOfContentsFilter do
expect(items[5].ancestors).to include(items[4])
end
end
+
+ context 'header text contains escaped content' do
+ let(:content) { '&lt;img src="x" onerror="alert(42)"&gt;' }
+ let(:results) { result(header(1, content)) }
+
+ it 'outputs escaped content' do
+ expect(doc.inner_html).to include(content)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index 64f3d09a25b..3a8667e434d 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -779,4 +779,12 @@ describe Gitlab::Auth::OAuth::User do
end
end
end
+
+ describe '#bypass_two_factor?' do
+ subject { oauth_user.bypass_two_factor? }
+
+ it 'returns always false' do
+ is_expected.to be_falsey
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
index bb950e6bbf8..76f49e778fb 100644
--- a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
@@ -37,4 +37,55 @@ describe Gitlab::Auth::Saml::AuthHash do
end
end
end
+
+ describe '#authn_context' do
+ let(:auth_hash_data) do
+ {
+ provider: 'saml',
+ uid: 'some_uid',
+ info:
+ {
+ name: 'mockuser',
+ email: 'mock@email.ch',
+ image: 'mock_user_thumbnail_url'
+ },
+ credentials:
+ {
+ token: 'mock_token',
+ secret: 'mock_secret'
+ },
+ extra:
+ {
+ raw_info:
+ {
+ info:
+ {
+ name: 'mockuser',
+ email: 'mock@email.ch',
+ image: 'mock_user_thumbnail_url'
+ }
+ }
+ }
+ }
+ end
+
+ subject(:saml_auth_hash) { described_class.new(OmniAuth::AuthHash.new(auth_hash_data)) }
+
+ context 'with response_object' do
+ before do
+ auth_hash_data[:extra][:response_object] = { document:
+ saml_xml(File.read('spec/fixtures/authentication/saml_response.xml')) }
+ end
+
+ it 'can extract authn_context' do
+ expect(saml_auth_hash.authn_context).to eq 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
+ end
+ end
+
+ context 'without response_object' do
+ it 'returns an empty string' do
+ expect(saml_auth_hash.authn_context).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/saml/user_spec.rb b/spec/lib/gitlab/auth/saml/user_spec.rb
index 62514ca0688..c523f5e177f 100644
--- a/spec/lib/gitlab/auth/saml/user_spec.rb
+++ b/spec/lib/gitlab/auth/saml/user_spec.rb
@@ -400,4 +400,45 @@ describe Gitlab::Auth::Saml::User do
end
end
end
+
+ describe '#bypass_two_factor?' do
+ let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
+
+ subject { saml_user.bypass_two_factor? }
+
+ context 'with authn_contexts_worth_two_factors configured' do
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config])
+ end
+
+ it 'returns true when authn_context is worth two factors' do
+ allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when authn_context is not worth two factors' do
+ allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
+ is_expected.to be_falsey
+ end
+
+ it 'returns false when authn_context is blank' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'without auth_contexts_worth_two_factors_configured' do
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
+ end
+
+ it 'returns false when authn_context is present' do
+ allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
+ is_expected.to be_falsey
+ end
+
+ it 'returns false when authn_context is blank' do
+ is_expected.to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index c3f528dd6fc..ed6fa3d229f 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -25,7 +25,9 @@ describe Gitlab::BitbucketImport::ProjectCreator do
end
it 'creates project' do
- allow_any_instance_of(Project).to receive(:add_import_job)
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
+ end
project_creator = described_class.new(repo, 'vim', namespace, user, access_params)
project = project_creator.execute
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index e79f0a7f257..adb3ff4321f 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -1,19 +1,69 @@
require 'spec_helper'
describe Gitlab::Ci::Variables::Collection::Item do
+ let(:variable_key) { 'VAR' }
+ let(:variable_value) { 'something' }
+ let(:expected_value) { variable_value }
+
let(:variable) do
- { key: 'VAR', value: 'something', public: true }
+ { key: variable_key, value: variable_value, public: true }
end
describe '.new' do
- it 'raises error if unknown key i specified' do
- expect { described_class.new(key: 'VAR', value: 'abc', files: true) }
- .to raise_error ArgumentError, 'unknown keyword: files'
+ context 'when unknown keyword is specified' do
+ it 'raises error' do
+ expect { described_class.new(key: variable_key, value: 'abc', files: true) }
+ .to raise_error ArgumentError, 'unknown keyword: files'
+ end
+ end
+
+ context 'when required keywords are not specified' do
+ it 'raises error' do
+ expect { described_class.new(key: variable_key) }
+ .to raise_error ArgumentError, 'missing keyword: value'
+ end
end
- it 'raises error when required keywords are not specified' do
- expect { described_class.new(key: 'VAR') }
- .to raise_error ArgumentError, 'missing keyword: value'
+ shared_examples 'creates variable' do
+ subject { described_class.new(key: variable_key, value: variable_value) }
+
+ it 'saves given value' do
+ expect(subject[:key]).to eq variable_key
+ expect(subject[:value]).to eq expected_value
+ end
+ end
+
+ shared_examples 'raises error for invalid type' do
+ it do
+ expect { described_class.new(key: variable_key, value: variable_value) }
+ .to raise_error ArgumentError, /`value` must be of type String, while it was:/
+ end
+ end
+
+ it_behaves_like 'creates variable'
+
+ context "when it's nil" do
+ let(:variable_value) { nil }
+ let(:expected_value) { nil }
+
+ it_behaves_like 'creates variable'
+ end
+
+ context "when it's an empty string" do
+ let(:variable_value) { '' }
+ let(:expected_value) { '' }
+
+ it_behaves_like 'creates variable'
+ end
+
+ context 'when provided value is not a string' do
+ [1, false, [], {}, Object.new].each do |val|
+ context "when it's #{val}" do
+ let(:variable_value) { val }
+
+ it_behaves_like 'raises error for invalid type'
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index cb2f7718c9c..5c91816a586 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Variables::Collection do
end
it 'appends an internal resource' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
subject.append(collection.first)
@@ -74,15 +74,15 @@ describe Gitlab::Ci::Variables::Collection do
describe '#+' do
it 'makes it possible to combine with an array' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
variables = [{ key: 'TEST', value: 'something' }]
expect((collection + variables).count).to eq 2
end
it 'makes it possible to combine with another collection' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
- other = described_class.new([{ key: 'TEST', value: 2 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
+ other = described_class.new([{ key: 'TEST', value: '2' }])
expect((collection + other).count).to eq 2
end
@@ -90,10 +90,10 @@ describe Gitlab::Ci::Variables::Collection do
describe '#to_runner_variables' do
it 'creates an array of hashes in a runner-compatible format' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
expect(collection.to_runner_variables)
- .to eq [{ key: 'TEST', value: 1, public: true }]
+ .to eq [{ key: 'TEST', value: '1', public: true }]
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 280f799f2ab..eb7148ff108 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1178,6 +1178,61 @@ describe Gitlab::Database::MigrationHelpers do
end
end
+ describe '#rename_column_using_background_migration' do
+ let!(:issue) { create(:issue, :closed, closed_at: Time.zone.now) }
+
+ it 'renames a column using a background migration' do
+ expect(model)
+ .to receive(:add_column)
+ .with(
+ 'issues',
+ :closed_at_timestamp,
+ :datetime_with_timezone,
+ limit: anything,
+ precision: anything,
+ scale: anything
+ )
+
+ expect(model)
+ .to receive(:install_rename_triggers)
+ .with('issues', :closed_at, :closed_at_timestamp)
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in)
+ .ordered
+ .with(
+ 10.minutes,
+ 'CopyColumn',
+ ['issues', :closed_at, :closed_at_timestamp, issue.id, issue.id]
+ )
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in)
+ .ordered
+ .with(
+ 1.hour + 10.minutes,
+ 'CleanupConcurrentRename',
+ ['issues', :closed_at, :closed_at_timestamp]
+ )
+
+ expect(Gitlab::BackgroundMigration)
+ .to receive(:steal)
+ .ordered
+ .with('CopyColumn')
+
+ expect(Gitlab::BackgroundMigration)
+ .to receive(:steal)
+ .ordered
+ .with('CleanupConcurrentRename')
+
+ model.rename_column_using_background_migration(
+ 'issues',
+ :closed_at,
+ :closed_at_timestamp
+ )
+ end
+ end
+
describe '#perform_background_migration_inline?' do
it 'returns true in a test environment' do
allow(Rails.env)
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 6015086f002..b6061df349d 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- shared_examples 'finding blobs' do
+ describe '.find' do
context 'nil path' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, nil) }
@@ -125,16 +125,6 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- describe '.find' do
- context 'when project_raw_show Gitaly feature is enabled' do
- it_behaves_like 'finding blobs'
- end
-
- context 'when project_raw_show Gitaly feature is disabled', :skip_gitaly_mock do
- it_behaves_like 'finding blobs'
- end
- end
-
shared_examples 'finding blobs by ID' do
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
let(:bad_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::BigCommit::ID) }
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 595482f76d5..b78fe4ba310 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1043,50 +1043,40 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#raw_changes_between' do
- shared_examples 'raw changes' do
- let(:old_rev) { }
- let(:new_rev) { }
- let(:changes) { repository.raw_changes_between(old_rev, new_rev) }
+ let(:old_rev) { }
+ let(:new_rev) { }
+ let(:changes) { repository.raw_changes_between(old_rev, new_rev) }
- context 'initial commit' do
- let(:old_rev) { Gitlab::Git::BLANK_SHA }
- let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
+ context 'initial commit' do
+ let(:old_rev) { Gitlab::Git::BLANK_SHA }
+ let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
- it 'returns the changes' do
- expect(changes).to be_present
- expect(changes.size).to eq(3)
- end
- end
-
- context 'with an invalid rev' do
- let(:old_rev) { 'foo' }
- let(:new_rev) { 'bar' }
-
- it 'returns an error' do
- expect { changes }.to raise_error(Gitlab::Git::Repository::GitError)
- end
+ it 'returns the changes' do
+ expect(changes).to be_present
+ expect(changes.size).to eq(3)
end
+ end
- context 'with valid revs' do
- let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
- let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+ context 'with an invalid rev' do
+ let(:old_rev) { 'foo' }
+ let(:new_rev) { 'bar' }
- it 'returns the changes' do
- expect(changes.size).to eq(9)
- expect(changes.first.operation).to eq(:modified)
- expect(changes.first.new_path).to eq('.gitmodules')
- expect(changes.last.operation).to eq(:added)
- expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png')
- end
+ it 'returns an error' do
+ expect { changes }.to raise_error(Gitlab::Git::Repository::GitError)
end
end
- context 'when gitaly is enabled' do
- it_behaves_like 'raw changes'
- end
+ context 'with valid revs' do
+ let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
+ let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
- context 'when gitaly is disabled', :disable_gitaly do
- it_behaves_like 'raw changes'
+ it 'returns the changes' do
+ expect(changes.size).to eq(9)
+ expect(changes.first.operation).to eq(:modified)
+ expect(changes.first.new_path).to eq('.gitmodules')
+ expect(changes.last.operation).to eq(:added)
+ expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png')
+ end
end
end
@@ -1881,49 +1871,39 @@ describe Gitlab::Git::Repository, seed_helper: true do
repository_rugged.config["gitlab.fullpath"] = repository_path
end
- shared_examples 'writing repo config' do
- context 'is given a path' do
- it 'writes it to disk' do
- repository.write_config(full_path: "not-the/real-path.git")
+ context 'is given a path' do
+ it 'writes it to disk' do
+ repository.write_config(full_path: "not-the/real-path.git")
- config = File.read(File.join(repository_path, "config"))
+ config = File.read(File.join(repository_path, "config"))
- expect(config).to include("[gitlab]")
- expect(config).to include("fullpath = not-the/real-path.git")
- end
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = not-the/real-path.git")
end
+ end
- context 'it is given an empty path' do
- it 'does not write it to disk' do
- repository.write_config(full_path: "")
+ context 'it is given an empty path' do
+ it 'does not write it to disk' do
+ repository.write_config(full_path: "")
- config = File.read(File.join(repository_path, "config"))
+ config = File.read(File.join(repository_path, "config"))
- expect(config).to include("[gitlab]")
- expect(config).to include("fullpath = #{repository_path}")
- end
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = #{repository_path}")
end
+ end
- context 'repository does not exist' do
- it 'raises NoRepository and does not call Gitaly WriteConfig' do
- repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '')
+ context 'repository does not exist' do
+ it 'raises NoRepository and does not call Gitaly WriteConfig' do
+ repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '')
- expect(repository.gitaly_repository_client).not_to receive(:write_config)
+ expect(repository.gitaly_repository_client).not_to receive(:write_config)
- expect do
- repository.write_config(full_path: 'foo/bar.git')
- end.to raise_error(Gitlab::Git::Repository::NoRepository)
- end
+ expect do
+ repository.write_config(full_path: 'foo/bar.git')
+ end.to raise_error(Gitlab::Git::Repository::NoRepository)
end
end
-
- context "when gitaly_write_config is enabled" do
- it_behaves_like "writing repo config"
- end
-
- context "when gitaly_write_config is disabled", :disable_gitaly do
- it_behaves_like "writing repo config"
- end
end
describe '#merge' do
@@ -2170,43 +2150,33 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#create_from_bundle' do
- shared_examples 'creating repo from bundle' do
- let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
- let(:project) { create(:project) }
- let(:imported_repo) { project.repository.raw }
-
- before do
- expect(repository.bundle_to_disk(bundle_path)).to be true
- end
-
- after do
- FileUtils.rm_rf(bundle_path)
- end
+ let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
+ let(:project) { create(:project) }
+ let(:imported_repo) { project.repository.raw }
- it 'creates a repo from a bundle file' do
- expect(imported_repo).not_to exist
+ before do
+ expect(repository.bundle_to_disk(bundle_path)).to be_truthy
+ end
- result = imported_repo.create_from_bundle(bundle_path)
+ after do
+ FileUtils.rm_rf(bundle_path)
+ end
- expect(result).to be true
- expect(imported_repo).to exist
- expect { imported_repo.fsck }.not_to raise_exception
- end
+ it 'creates a repo from a bundle file' do
+ expect(imported_repo).not_to exist
- it 'creates a symlink to the global hooks dir' do
- imported_repo.create_from_bundle(bundle_path)
- hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') }
+ result = imported_repo.create_from_bundle(bundle_path)
- expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
- end
+ expect(result).to be_truthy
+ expect(imported_repo).to exist
+ expect { imported_repo.fsck }.not_to raise_exception
end
- context 'when Gitaly create_repo_from_bundle feature is enabled' do
- it_behaves_like 'creating repo from bundle'
- end
+ it 'creates a symlink to the global hooks dir' do
+ imported_repo.create_from_bundle(bundle_path)
+ hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') }
- context 'when Gitaly create_repo_from_bundle feature is disabled', :disable_gitaly do
- it_behaves_like 'creating repo from bundle'
+ expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
end
end
diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
index 5ea086e4abd..b814f5fc76c 100644
--- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
@@ -21,7 +21,9 @@ describe Gitlab::GitlabImport::ProjectCreator do
end
it 'creates project' do
- allow_any_instance_of(Project).to receive(:add_import_job)
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
+ end
project_creator = described_class.new(repo, namespace, user, access_params)
project = project_creator.execute
diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
index 24cd518c77b..b959e006292 100644
--- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
@@ -16,7 +16,9 @@ describe Gitlab::GoogleCodeImport::ProjectCreator do
end
it 'creates project' do
- allow_any_instance_of(Project).to receive(:add_import_job)
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
+ end
project_creator = described_class.new(repo, namespace, user)
project = project_creator.execute
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index 013b8895f67..7ffa84f906d 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::ImportExport::RepoRestorer do
end
it 'restores the repo successfully' do
- expect(restorer.restore).to be true
+ expect(restorer.restore).to be_truthy
end
it 'has the webhooks' do
diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
index 972b17d5b12..3d4240fa4ba 100644
--- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
@@ -17,7 +17,10 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do
before do
namespace.add_owner(user)
- allow_any_instance_of(Project).to receive(:add_import_job)
+
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
+ end
end
describe '#execute' do
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 091645ee86f..7972ff253fe 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -45,7 +45,7 @@ describe Gitlab::Metrics::Samplers::RubySampler do
it 'adds a metric containing garbage collection time statistics' do
expect(GC::Profiler).to receive(:total_time).and_return(0.24)
- expect(sampler.metrics[:total_time]).to receive(:set).with({}, 240)
+ expect(sampler.metrics[:total_time]).to receive(:increment).with({}, 0.24)
sampler.sample
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 22d921716aa..20def4fefe2 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -29,20 +29,20 @@ describe Gitlab::UsageData do
active_user_count
counts
recorded_at
- mattermost_enabled
edition
version
installation_type
uuid
hostname
- signup
- ldap
- gravatar
- omniauth
- reply_by_email
- container_registry
+ mattermost_enabled
+ signup_enabled
+ ldap_enabled
+ gravatar_enabled
+ omniauth_enabled
+ reply_by_email_enabled
+ container_registry_enabled
+ gitlab_shared_runners_enabled
gitlab_pages
- gitlab_shared_runners
git
database
avg_cycle_analytics
@@ -129,13 +129,14 @@ describe Gitlab::UsageData do
subject { described_class.features_usage_data_ce }
it 'gathers feature usage data' do
- expect(subject[:signup]).to eq(Gitlab::CurrentSettings.allow_signup?)
- expect(subject[:ldap]).to eq(Gitlab.config.ldap.enabled)
- expect(subject[:gravatar]).to eq(Gitlab::CurrentSettings.gravatar_enabled?)
- expect(subject[:omniauth]).to eq(Gitlab.config.omniauth.enabled)
- expect(subject[:reply_by_email]).to eq(Gitlab::IncomingEmail.enabled?)
- expect(subject[:container_registry]).to eq(Gitlab.config.registry.enabled)
- expect(subject[:gitlab_shared_runners]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled)
+ expect(subject[:mattermost_enabled]).to eq(Gitlab.config.mattermost.enabled)
+ expect(subject[:signup_enabled]).to eq(Gitlab::CurrentSettings.allow_signup?)
+ expect(subject[:ldap_enabled]).to eq(Gitlab.config.ldap.enabled)
+ expect(subject[:gravatar_enabled]).to eq(Gitlab::CurrentSettings.gravatar_enabled?)
+ expect(subject[:omniauth_enabled]).to eq(Gitlab.config.omniauth.enabled)
+ expect(subject[:reply_by_email_enabled]).to eq(Gitlab::IncomingEmail.enabled?)
+ expect(subject[:container_registry_enabled]).to eq(Gitlab.config.registry.enabled)
+ expect(subject[:gitlab_shared_runners_enabled]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled)
end
end
diff --git a/spec/lib/gitlab/verify/uploads_spec.rb b/spec/lib/gitlab/verify/uploads_spec.rb
index 296866d3319..38c30fab1ba 100644
--- a/spec/lib/gitlab/verify/uploads_spec.rb
+++ b/spec/lib/gitlab/verify/uploads_spec.rb
@@ -47,20 +47,49 @@ describe Gitlab::Verify::Uploads do
before do
stub_uploads_object_storage(AvatarUploader)
upload.update!(store: ObjectStorage::Store::REMOTE)
- expect(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file)
end
- it 'passes uploads in object storage that exist' do
- expect(file).to receive(:exists?).and_return(true)
+ describe 'returned hash object' do
+ before do
+ expect(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file)
+ end
- expect(failures).to eq({})
+ it 'passes uploads in object storage that exist' do
+ expect(file).to receive(:exists?).and_return(true)
+
+ expect(failures).to eq({})
+ end
+
+ it 'fails uploads in object storage that do not exist' do
+ expect(file).to receive(:exists?).and_return(false)
+
+ expect(failures.keys).to contain_exactly(upload)
+ expect(failure).to include('Remote object does not exist')
+ end
end
- it 'fails uploads in object storage that do not exist' do
- expect(file).to receive(:exists?).and_return(false)
+ describe 'performance' do
+ before do
+ allow(file).to receive(:exists?)
+ allow(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file)
+ end
+
+ it "avoids N+1 queries" do
+ control_count = ActiveRecord::QueryRecorder.new { perform_task }
+
+ # Create additional uploads in object storage
+ projects = create_list(:project, 3, :with_avatar)
+ uploads = projects.flat_map(&:uploads)
+ uploads.each do |upload|
+ upload.update!(store: ObjectStorage::Store::REMOTE)
+ end
+
+ expect { perform_task }.not_to exceed_query_limit(control_count)
+ end
- expect(failures.keys).to contain_exactly(upload)
- expect(failure).to include('Remote object does not exist')
+ def perform_task
+ described_class.new(batch_size: 100).run_batches { }
+ end
end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 775ca4ba0eb..a9a45367b4a 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -416,16 +416,10 @@ describe Notify do
end
it 'has the correct subject and body' do
- reasons = %w[foo bar]
-
- allow_any_instance_of(MergeRequestPresenter).to receive(:unmergeable_reasons).and_return(reasons)
aggregate_failures do
is_expected.to have_referable_subject(merge_request, reply: true)
is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- is_expected.to have_body_text('following reasons:')
- reasons.each do |reason|
- is_expected.to have_body_text(reason)
- end
+ is_expected.to have_body_text('due to conflict.')
end
end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 3e6656e0f12..02f74e2ea54 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -25,15 +25,6 @@ describe ApplicationSetting do
it { is_expected.to allow_value(https).for(:after_sign_out_path) }
it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) }
- describe 'disabled_oauth_sign_in_sources validations' do
- before do
- allow(Devise).to receive(:omniauth_providers).and_return([:github])
- end
-
- it { is_expected.to allow_value(['github']).for(:disabled_oauth_sign_in_sources) }
- it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) }
- end
-
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
@@ -314,6 +305,33 @@ describe ApplicationSetting do
end
end
+ describe '#disabled_oauth_sign_in_sources=' do
+ before do
+ allow(Devise).to receive(:omniauth_providers).and_return([:github])
+ end
+
+ it 'removes unknown sources (as strings) from the array' do
+ subject.disabled_oauth_sign_in_sources = %w[github test]
+
+ expect(subject).to be_valid
+ expect(subject.disabled_oauth_sign_in_sources).to eq ['github']
+ end
+
+ it 'removes unknown sources (as symbols) from the array' do
+ subject.disabled_oauth_sign_in_sources = %i[github test]
+
+ expect(subject).to be_valid
+ expect(subject.disabled_oauth_sign_in_sources).to eq ['github']
+ end
+
+ it 'ignores nil' do
+ subject.disabled_oauth_sign_in_sources = nil
+
+ expect(subject).to be_valid
+ expect(subject.disabled_oauth_sign_in_sources).to be_empty
+ end
+ end
+
context 'restricted signup domains' do
it 'sets single domain' do
setting.domain_whitelist_raw = 'example.com'
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 51b9b518117..6758adc59eb 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1871,7 +1871,11 @@ describe Ci::Build do
end
context 'when yaml_variables are undefined' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id,
+ ref: project.default_branch)
+ end
before do
build.yaml_variables = nil
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index b5a6d959ccb..c5d550cba1b 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -125,14 +125,6 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
end
-
- context 'when data_store is others' do
- before do
- build_trace_chunk.send(:write_attribute, :data_store, -1)
- end
-
- it { expect { subject }.to raise_error('Unsupported data store') }
- end
end
describe '#truncate' do
diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb
index b821a84d5e0..39c16ae60af 100644
--- a/spec/models/concerns/sortable_spec.rb
+++ b/spec/models/concerns/sortable_spec.rb
@@ -40,15 +40,25 @@ describe Sortable do
describe 'ordering by name' do
it 'ascending' do
- expect(relation).to receive(:reorder).with("lower(name) asc")
+ expect(relation).to receive(:reorder).once.and_call_original
- relation.order_by('name_asc')
+ table = Regexp.escape(ActiveRecord::Base.connection.quote_table_name(:namespaces))
+ column = Regexp.escape(ActiveRecord::Base.connection.quote_column_name(:name))
+
+ sql = relation.order_by('name_asc').to_sql
+
+ expect(sql).to match /.+ORDER BY LOWER\(#{table}.#{column}\) ASC\z/
end
it 'descending' do
- expect(relation).to receive(:reorder).with("lower(name) desc")
+ expect(relation).to receive(:reorder).once.and_call_original
+
+ table = Regexp.escape(ActiveRecord::Base.connection.quote_table_name(:namespaces))
+ column = Regexp.escape(ActiveRecord::Base.connection.quote_column_name(:name))
+
+ sql = relation.order_by('name_desc').to_sql
- relation.order_by('name_desc')
+ expect(sql).to match /.+ORDER BY LOWER\(#{table}.#{column}\) DESC\z/
end
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index b4249d72fc8..48c01fc4d4e 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -47,6 +47,45 @@ describe MergeRequestDiff do
end
describe '#diffs' do
+ let(:merge_request) { create(:merge_request, :with_diffs) }
+ let!(:diff) { merge_request.merge_request_diff.reload }
+
+ context 'when it was not cleaned by the system' do
+ it 'returns persisted diffs' do
+ expect(diff).to receive(:load_diffs)
+
+ diff.diffs
+ end
+ end
+
+ context 'when diff was cleaned by the system' do
+ before do
+ diff.clean!
+ end
+
+ it 'returns diffs from repository if can compare with current diff refs' do
+ expect(diff).not_to receive(:load_diffs)
+
+ expect(Compare)
+ .to receive(:new)
+ .with(instance_of(Gitlab::Git::Compare), merge_request.target_project,
+ base_sha: diff.base_commit_sha, straight: false)
+ .and_call_original
+
+ diff.diffs
+ end
+
+ it 'returns persisted diffs if cannot compare with diff refs' do
+ expect(diff).to receive(:load_diffs)
+
+ diff.update!(head_commit_sha: 'invalid-sha')
+
+ diff.diffs
+ end
+ end
+ end
+
+ describe '#raw_diffs' do
context 'when the :ignore_whitespace_change option is set' do
it 'creates a new compare object instead of loading from the DB' do
expect(diff_with_commits).not_to receive(:load_diffs)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 3f028b3bd5c..ec72fefd137 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1324,6 +1324,7 @@ describe MergeRequest do
context 'when broken' do
before do
allow(subject).to receive(:broken?) { true }
+ allow(project.repository).to receive(:can_be_merged?).and_return(false)
end
it 'becomes unmergeable' do
@@ -1629,28 +1630,17 @@ describe MergeRequest do
end
describe "#reload_diff" do
- let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
- let(:commit) { subject.project.commit(sample_commit.id) }
-
- it "does not change existing merge request diff" do
- expect(subject.merge_request_diff).not_to receive(:save_git_content)
- subject.reload_diff
- end
-
- it "creates new merge request diff" do
- expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
- end
-
- it "executes diff cache service" do
- expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject, an_instance_of(MergeRequestDiff))
+ it 'calls MergeRequests::ReloadDiffsService#execute with correct params' do
+ user = create(:user)
+ service = instance_double(MergeRequests::ReloadDiffsService, execute: nil)
- subject.reload_diff
- end
+ expect(MergeRequests::ReloadDiffsService)
+ .to receive(:new).with(subject, user)
+ .and_return(service)
- it "calls update_diff_discussion_positions" do
- expect(subject).to receive(:update_diff_discussion_positions)
+ subject.reload_diff(user)
- subject.reload_diff
+ expect(service).to have_received(:execute)
end
context 'when using the after_update hook to update' do
@@ -2144,32 +2134,61 @@ describe MergeRequest do
describe 'transition to cannot_be_merged' do
let(:notification_service) { double(:notification_service) }
let(:todo_service) { double(:todo_service) }
-
- subject { create(:merge_request, merge_status: :unchecked) }
+ subject { create(:merge_request, state, merge_status: :unchecked) }
before do
allow(NotificationService).to receive(:new).and_return(notification_service)
allow(TodoService).to receive(:new).and_return(todo_service)
+
+ allow(subject.project.repository).to receive(:can_be_merged?).and_return(false)
end
- it 'notifies, but does not notify again if rechecking still results in cannot_be_merged' do
- expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once
- expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once
+ [:opened, :locked].each do |state|
+ context state do
+ let(:state) { state }
+
+ it 'notifies conflict, but does not notify again if rechecking still results in cannot_be_merged' do
+ expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once
+ expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once
- subject.mark_as_unmergeable
- subject.mark_as_unchecked
- subject.mark_as_unmergeable
+ subject.mark_as_unmergeable
+ subject.mark_as_unchecked
+ subject.mark_as_unmergeable
+ end
+
+ it 'notifies conflict, whenever newly unmergeable' do
+ expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice
+ expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice
+
+ subject.mark_as_unmergeable
+ subject.mark_as_unchecked
+ subject.mark_as_mergeable
+ subject.mark_as_unchecked
+ subject.mark_as_unmergeable
+ end
+
+ it 'does not notify whenever merge request is newly unmergeable due to other reasons' do
+ allow(subject.project.repository).to receive(:can_be_merged?).and_return(true)
+
+ expect(notification_service).not_to receive(:merge_request_unmergeable)
+ expect(todo_service).not_to receive(:merge_request_became_unmergeable)
+
+ subject.mark_as_unmergeable
+ end
+ end
end
- it 'notifies whenever merge request is newly unmergeable' do
- expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice
- expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice
+ [:closed, :merged].each do |state|
+ let(:state) { state }
- subject.mark_as_unmergeable
- subject.mark_as_unchecked
- subject.mark_as_mergeable
- subject.mark_as_unchecked
- subject.mark_as_unmergeable
+ context state do
+ it 'does not notify' do
+ expect(notification_service).not_to receive(:merge_request_unmergeable)
+ expect(todo_service).not_to receive(:merge_request_became_unmergeable)
+
+ subject.mark_as_unmergeable
+ end
+ end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 18b01c3e6b7..70f1a1c8b38 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -655,6 +655,19 @@ describe Namespace do
end
end
+ describe '#root_ancestor' do
+ it 'returns the top most ancestor', :nested_groups do
+ root_group = create(:group)
+ nested_group = create(:group, parent: root_group)
+ deep_nested_group = create(:group, parent: nested_group)
+ very_deep_nested_group = create(:group, parent: deep_nested_group)
+
+ expect(nested_group.root_ancestor).to eq(root_group)
+ expect(deep_nested_group.root_ancestor).to eq(root_group)
+ expect(very_deep_nested_group.root_ancestor).to eq(root_group)
+ end
+ end
+
describe '#remove_exports' do
let(:legacy_project) { create(:project, :with_export, :legacy_storage, namespace: namespace) }
let(:hashed_project) { create(:project, :with_export, namespace: namespace) }
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 6a6c71e6c82..a2cb716cb93 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -828,5 +828,15 @@ describe Note do
note.destroy!
end
+
+ context 'when issuable etag caching is disabled' do
+ it 'does not store cache key' do
+ allow(note.noteable).to receive(:etag_caching_enabled?).and_return(false)
+
+ expect_any_instance_of(Gitlab::EtagCaching::Store).not_to receive(:touch)
+
+ note.save!
+ end
+ end
end
end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index d5fb4a7742c..e3b37739e8e 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -70,41 +70,6 @@ describe MergeRequestPresenter do
end
end
- describe "#unmergeable_reasons" do
- let(:presenter) { described_class.new(resource, current_user: user) }
-
- before do
- # Mergeable base state
- allow(resource).to receive(:has_no_commits?).and_return(false)
- allow(resource).to receive(:source_branch_exists?).and_return(true)
- allow(resource).to receive(:target_branch_exists?).and_return(true)
- allow(resource.project.repository).to receive(:can_be_merged?).and_return(true)
- end
-
- it "handles mergeable request" do
- expect(presenter.unmergeable_reasons).to eq([])
- end
-
- it "handles no commit" do
- allow(resource).to receive(:has_no_commits?).and_return(true)
-
- expect(presenter.unmergeable_reasons).to eq(["no commits"])
- end
-
- it "handles branches missing" do
- allow(resource).to receive(:source_branch_exists?).and_return(false)
- allow(resource).to receive(:target_branch_exists?).and_return(false)
-
- expect(presenter.unmergeable_reasons).to eq(["source branch is missing", "target branch is missing"])
- end
-
- it "handles merge conflict" do
- allow(resource.project.repository).to receive(:can_be_merged?).and_return(false)
-
- expect(presenter.unmergeable_reasons).to eq(["has merge conflicts"])
- end
- end
-
describe '#conflict_resolution_path' do
let(:project) { create :project }
let(:user) { create :user }
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 92b614b087e..7710f19ce4e 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe API::Boards do
set(:user) { create(:user) }
- set(:user2) { create(:user) }
set(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:admin) { create(:user, :admin) }
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 64f51d9843d..9bb6ed62393 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -155,6 +155,12 @@ describe API::Branches do
end
it_behaves_like 'repository branch'
+
+ it 'returns that the current user cannot push' do
+ get api(route, current_user)
+
+ expect(json_response['can_push']).to eq(false)
+ end
end
context 'when unauthenticated', 'and project is private' do
@@ -169,6 +175,12 @@ describe API::Branches do
it_behaves_like 'repository branch'
+ it 'returns that the current user can push' do
+ get api(route, current_user)
+
+ expect(json_response['can_push']).to eq(true)
+ end
+
context 'when branch contains a dot' do
let(:branch_name) { branch_with_dot.name }
@@ -202,6 +214,23 @@ describe API::Branches do
end
end
+ context 'when authenticated', 'as a developer and branch is protected' do
+ let(:current_user) { create(:user) }
+ let!(:protected_branch) { create(:protected_branch, project: project, name: branch_name) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'repository branch'
+
+ it 'returns that the current user cannot push' do
+ get api(route, current_user)
+
+ expect(json_response['can_push']).to eq(false)
+ end
+ end
+
context 'when authenticated', 'as a guest' do
it_behaves_like '403 response' do
let(:request) { get api(route, guest) }
diff --git a/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
new file mode 100644
index 00000000000..7f689b196c5
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/gitlab/finder_with_find_by'
+
+describe RuboCop::Cop::Gitlab::FinderWithFindBy do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when calling execute.find' do
+ let(:source) do
+ <<~SRC
+ DummyFinder.new(some_args)
+ .execute
+ .find_by!(1)
+ SRC
+ end
+ let(:corrected_source) do
+ <<~SRC
+ DummyFinder.new(some_args)
+ .find_by!(1)
+ SRC
+ end
+
+ it 'registers an offence' do
+ inspect_source(source)
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'can autocorrect the source' do
+ expect(autocorrect_source(source)).to eq(corrected_source)
+ end
+
+ context 'when called within the `FinderMethods` module' do
+ let(:source) do
+ <<~SRC
+ module FinderMethods
+ def find_by!(*args)
+ execute.find_by!(args)
+ end
+ end
+ SRC
+ end
+
+ it 'does not register an offence' do
+ inspect_source(source)
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/update_large_table_spec.rb b/spec/rubocop/cop/migration/update_large_table_spec.rb
index ef724fc8bad..5e08eb4f772 100644
--- a/spec/rubocop/cop/migration/update_large_table_spec.rb
+++ b/spec/rubocop/cop/migration/update_large_table_spec.rb
@@ -32,6 +32,14 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do
include_examples 'large tables', 'add_column_with_default'
end
+ context 'for the change_column_type_concurrently method' do
+ include_examples 'large tables', 'change_column_type_concurrently'
+ end
+
+ context 'for the rename_column_concurrently method' do
+ include_examples 'large tables', 'rename_column_concurrently'
+ end
+
context 'for the update_column_in_batches method' do
include_examples 'large tables', 'update_column_in_batches'
end
@@ -60,6 +68,18 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do
expect(cop.offenses).to be_empty
end
+ it 'registers no offense for change_column_type_concurrently' do
+ inspect_source("change_column_type_concurrently :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it 'registers no offense for update_column_in_batches' do
+ inspect_source("rename_column_concurrently :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+
it 'registers no offense for update_column_in_batches' do
inspect_source("add_column_with_default :#{table}, :column, default: true")
diff --git a/spec/serializers/blob_entity_spec.rb b/spec/serializers/blob_entity_spec.rb
new file mode 100644
index 00000000000..dde59ff72df
--- /dev/null
+++ b/spec/serializers/blob_entity_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe BlobEntity do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:blob) { project.commit('master').diffs.diff_files.first.blob }
+ let(:request) { EntityRequest.new(project: project, ref: 'master') }
+
+ let(:entity) do
+ described_class.new(blob, request: request)
+ end
+
+ context 'as json' do
+ subject { entity.as_json }
+
+ it 'exposes needed attributes' do
+ expect(subject).to include(:readable_text, :url)
+ end
+ end
+end
diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb
index 45d7c703df3..c4a6c117b76 100644
--- a/spec/serializers/diff_file_entity_spec.rb
+++ b/spec/serializers/diff_file_entity_spec.rb
@@ -9,16 +9,48 @@ describe DiffFileEntity do
let(:diff_refs) { commit.diff_refs }
let(:diff) { commit.raw_diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
- let(:entity) { described_class.new(diff_file) }
+ let(:entity) { described_class.new(diff_file, request: {}) }
subject { entity.as_json }
- it 'exposes correct attributes' do
- expect(subject).to include(
- :submodule, :submodule_link, :file_path,
- :deleted_file, :old_path, :new_path, :mode_changed,
- :a_mode, :b_mode, :text, :old_path_html,
- :new_path_html
- )
+ shared_examples 'diff file entity' do
+ it 'exposes correct attributes' do
+ expect(subject).to include(
+ :submodule, :submodule_link, :submodule_tree_url, :file_path,
+ :deleted_file, :old_path, :new_path, :mode_changed,
+ :a_mode, :b_mode, :text, :old_path_html,
+ :new_path_html, :highlighted_diff_lines, :parallel_diff_lines,
+ :blob, :file_hash, :added_lines, :removed_lines, :diff_refs, :content_sha,
+ :stored_externally, :external_storage, :too_large, :collapsed, :new_file,
+ :context_lines_path
+ )
+ end
+ end
+
+ context 'when there is no merge request' do
+ it_behaves_like 'diff file entity'
+ end
+
+ context 'when there is a merge request' do
+ let(:user) { create(:user) }
+ let(:request) { EntityRequest.new(project: project, current_user: user) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:entity) { described_class.new(diff_file, request: request, merge_request: merge_request) }
+ let(:exposed_urls) { %i(load_collapsed_diff_url edit_path view_path context_lines_path) }
+
+ it_behaves_like 'diff file entity'
+
+ it 'exposes additional attributes' do
+ expect(subject).to include(*exposed_urls)
+ expect(subject).to include(:replaced_view_path)
+ end
+
+ it 'points all urls to merge request target project' do
+ response = subject
+
+ exposed_urls.each do |attribute|
+ expect(response[attribute]).to include(merge_request.target_project.to_param)
+ end
+ end
end
end
diff --git a/spec/serializers/diffs_entity_spec.rb b/spec/serializers/diffs_entity_spec.rb
new file mode 100644
index 00000000000..19a843b0cb7
--- /dev/null
+++ b/spec/serializers/diffs_entity_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe DiffsEntity do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:request) { EntityRequest.new(project: project, current_user: user) }
+ let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let(:merge_request_diffs) { merge_request.merge_request_diffs }
+
+ let(:entity) do
+ described_class.new(merge_request_diffs.first.diffs, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs)
+ end
+
+ context 'as json' do
+ subject { entity.as_json }
+
+ it 'contains needed attributes' do
+ expect(subject).to include(
+ :real_size, :size, :branch_name,
+ :target_branch_name, :commit, :merge_request_diff,
+ :start_version, :latest_diff, :latest_version_path,
+ :added_lines, :removed_lines, :render_overflow_warning,
+ :email_patch_path, :plain_diff_path, :diff_files,
+ :merge_request_diffs
+ )
+ end
+ end
+end
diff --git a/spec/serializers/discussion_entity_spec.rb b/spec/serializers/discussion_entity_spec.rb
index 7e19e74ca00..44d8cc69d9b 100644
--- a/spec/serializers/discussion_entity_spec.rb
+++ b/spec/serializers/discussion_entity_spec.rb
@@ -19,10 +19,20 @@ describe DiscussionEntity do
end
it 'exposes correct attributes' do
- expect(subject).to include(
- :id, :expanded, :notes, :individual_note,
- :resolvable, :resolved, :resolve_path,
- :resolve_with_issue_path, :diff_discussion
+ expect(subject.keys.sort).to include(
+ :diff_discussion,
+ :expanded,
+ :id,
+ :individual_note,
+ :notes,
+ :resolvable,
+ :resolve_path,
+ :resolve_with_issue_path,
+ :resolved,
+ :discussion_path,
+ :resolved_at,
+ :for_commit,
+ :commit_id
)
end
@@ -30,7 +40,21 @@ describe DiscussionEntity do
let(:note) { create(:diff_note_on_merge_request) }
it 'exposes diff file attributes' do
- expect(subject).to include(:diff_file, :truncated_diff_lines, :image_diff_html)
+ expect(subject.keys.sort).to include(
+ :diff_file,
+ :truncated_diff_lines,
+ :position,
+ :line_code,
+ :active
+ )
+ end
+
+ context 'when diff file is a image' do
+ it 'exposes image attributes' do
+ allow(discussion).to receive(:on_image?).and_return(true)
+
+ expect(subject.keys).to include(:image_diff_html)
+ end
end
end
end
diff --git a/spec/serializers/merge_request_diff_entity_spec.rb b/spec/serializers/merge_request_diff_entity_spec.rb
new file mode 100644
index 00000000000..84f6833d88a
--- /dev/null
+++ b/spec/serializers/merge_request_diff_entity_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe MergeRequestDiffEntity do
+ let(:project) { create(:project, :repository) }
+ let(:request) { EntityRequest.new(project: project) }
+ let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let(:merge_request_diffs) { merge_request.merge_request_diffs }
+
+ let(:entity) do
+ described_class.new(merge_request_diffs.first, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs)
+ end
+
+ context 'as json' do
+ subject { entity.as_json }
+
+ it 'exposes needed attributes' do
+ expect(subject).to include(
+ :version_index, :created_at, :commits_count,
+ :latest, :short_commit_sha, :version_path,
+ :compare_path
+ )
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_user_entity_spec.rb b/spec/serializers/merge_request_user_entity_spec.rb
new file mode 100644
index 00000000000..c91ea4aa681
--- /dev/null
+++ b/spec/serializers/merge_request_user_entity_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe MergeRequestUserEntity do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:request) { EntityRequest.new(project: project, current_user: user) }
+
+ let(:entity) do
+ described_class.new(user, request: request)
+ end
+
+ context 'as json' do
+ subject { entity.as_json }
+
+ it 'exposes needed attributes' do
+ expect(subject).to include(:can_fork, :can_create_merge_request, :fork_path)
+ end
+ end
+end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index da8e660c16b..fce73e0ac1f 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -21,6 +21,11 @@ describe Auth::ContainerRegistryAuthenticationService do
allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key)
end
+ shared_examples 'an authenticated' do
+ it { is_expected.to include(:token) }
+ it { expect(payload).to include('access') }
+ end
+
shared_examples 'a valid token' do
it { is_expected.to include(:token) }
it { expect(payload).to include('access') }
@@ -380,6 +385,14 @@ describe Auth::ContainerRegistryAuthenticationService do
current_project.add_developer(current_user)
end
+ context 'allow to use offline_token' do
+ let(:current_params) do
+ { offline_token: true }
+ end
+
+ it_behaves_like 'an authenticated'
+ end
+
it_behaves_like 'a valid token'
context 'allow to pull and push images' do
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index 13accc6ae1b..b6cfc09da65 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -31,10 +31,8 @@ describe Issues::ResolveDiscussions do
it "only queries for the merge request once" do
fake_finder = double
- fake_results = double
- expect(fake_finder).to receive(:execute).and_return(fake_results).exactly(1)
- expect(fake_results).to receive(:find_by).exactly(1)
+ expect(fake_finder).to receive(:find_by).exactly(1)
expect(MergeRequestsFinder).to receive(:new).and_return(fake_finder).exactly(1)
2.times { service.merge_request_to_resolve_discussions_of }
diff --git a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
new file mode 100644
index 00000000000..1c632847940
--- /dev/null
+++ b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe MergeRequests::DeleteNonLatestDiffsService, :clean_gitlab_redis_shared_state do
+ let(:merge_request) { create(:merge_request) }
+
+ let!(:subject) { described_class.new(merge_request) }
+
+ describe '#execute' do
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ 3.times { merge_request.create_merge_request_diff }
+ end
+
+ it 'schedules non-latest merge request diffs removal' do
+ diffs = merge_request.merge_request_diffs
+
+ expect(diffs.count).to eq(4)
+
+ Timecop.freeze do
+ expect(DeleteDiffFilesWorker)
+ .to receive(:bulk_perform_in)
+ .with(5.minutes, [[diffs.first.id], [diffs.second.id]])
+ expect(DeleteDiffFilesWorker)
+ .to receive(:bulk_perform_in)
+ .with(10.minutes, [[diffs.third.id]])
+
+ subject.execute
+ end
+ end
+
+ it 'schedules no removal if it is already cleaned' do
+ merge_request.merge_request_diffs.each(&:clean!)
+
+ expect(DeleteDiffFilesWorker).not_to receive(:bulk_perform_in)
+
+ subject.execute
+ end
+
+ it 'schedules no removal if it is empty' do
+ merge_request.merge_request_diffs.each { |diff| diff.update!(state: :empty) }
+
+ expect(DeleteDiffFilesWorker).not_to receive(:bulk_perform_in)
+
+ subject.execute
+ end
+
+ it 'schedules no removal if there is no non-latest diffs' do
+ merge_request
+ .merge_request_diffs
+ .where.not(id: merge_request.latest_merge_request_diff_id)
+ .destroy_all
+
+ expect(DeleteDiffFilesWorker).not_to receive(:bulk_perform_in)
+
+ subject.execute
+ end
+ end
+end
diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
deleted file mode 100644
index 57b6165cfb0..00000000000
--- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-describe MergeRequests::MergeRequestDiffCacheService, :use_clean_rails_memory_store_caching do
- let(:subject) { described_class.new }
- let(:merge_request) { create(:merge_request) }
-
- describe '#execute' do
- before do
- allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true)
- allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true)
- end
-
- it 'retrieves the diff files to cache the highlighted result' do
- new_diff = merge_request.merge_request_diff
- cache_key = new_diff.diffs.cache_key
-
- expect(Rails.cache).to receive(:read).with(cache_key).and_call_original
- expect(Rails.cache).to receive(:write).with(cache_key, anything, anything).and_call_original
-
- subject.execute(merge_request, new_diff)
- end
-
- it 'clears the cache for older diffs on the merge request' do
- old_diff = merge_request.merge_request_diff
- old_cache_key = old_diff.diffs.cache_key
-
- subject.execute(merge_request, old_diff)
-
- new_diff = merge_request.create_merge_request_diff
- new_cache_key = new_diff.diffs.cache_key
-
- expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original
- expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original
- expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original
-
- subject.execute(merge_request, new_diff)
- end
- end
-end
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index 70957431942..790ecce8ded 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -35,5 +35,17 @@ describe MergeRequests::PostMergeService do
described_class.new(project, user, {}).execute(merge_request)
end
+
+ it 'deletes non-latest diffs' do
+ diff_removal_service = instance_double(MergeRequests::DeleteNonLatestDiffsService, execute: nil)
+
+ expect(MergeRequests::DeleteNonLatestDiffsService)
+ .to receive(:new).with(merge_request)
+ .and_return(diff_removal_service)
+
+ described_class.new(project, user, {}).execute(merge_request)
+
+ expect(diff_removal_service).to have_received(:execute)
+ end
end
end
diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb
new file mode 100644
index 00000000000..a0a27d247fc
--- /dev/null
+++ b/spec/services/merge_requests/reload_diffs_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_caching do
+ let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:subject) { described_class.new(merge_request, current_user) }
+
+ describe '#execute' do
+ it 'creates new merge request diff' do
+ expect { subject.execute }.to change { merge_request.merge_request_diffs.count }.by(1)
+ end
+
+ it 'calls update_diff_discussion_positions with correct params' do
+ old_diff_refs = merge_request.diff_refs
+ new_diff = merge_request.create_merge_request_diff
+ new_diff_refs = merge_request.diff_refs
+
+ expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
+ expect(merge_request).to receive(:update_diff_discussion_positions)
+ .with(old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ current_user: current_user)
+
+ subject.execute
+ end
+
+ it 'does not change existing merge request diff' do
+ expect(merge_request.merge_request_diff).not_to receive(:save_git_content)
+
+ subject.execute
+ end
+
+ context 'cache clearing' do
+ before do
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true)
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true)
+ end
+
+ it 'retrieves the diff files to cache the highlighted result' do
+ new_diff = merge_request.create_merge_request_diff
+ cache_key = new_diff.diffs_collection.cache_key
+
+ expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
+ expect(Rails.cache).to receive(:read).with(cache_key).and_call_original
+ expect(Rails.cache).to receive(:write).with(cache_key, anything, anything).and_call_original
+
+ subject.execute
+ end
+
+ it 'clears the cache for older diffs on the merge request' do
+ old_diff = merge_request.merge_request_diff
+ old_cache_key = old_diff.diffs_collection.cache_key
+ new_diff = merge_request.create_merge_request_diff
+ new_cache_key = new_diff.diffs_collection.cache_key
+
+ expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
+ expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original
+ expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original
+ expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original
+ subject.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/batch_open_issues_count_service_spec.rb b/spec/services/projects/batch_open_issues_count_service_spec.rb
new file mode 100644
index 00000000000..599aaf62080
--- /dev/null
+++ b/spec/services/projects/batch_open_issues_count_service_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Projects::BatchOpenIssuesCountService do
+ let!(:project_1) { create(:project) }
+ let!(:project_2) { create(:project) }
+
+ let(:subject) { described_class.new([project_1, project_2]) }
+
+ context '#refresh_cache', :use_clean_rails_memory_store_caching do
+ before do
+ create(:issue, project: project_1)
+ create(:issue, project: project_1, confidential: true)
+
+ create(:issue, project: project_2)
+ create(:issue, project: project_2, confidential: true)
+ end
+
+ context 'when cache is clean' do
+ it 'refreshes cache keys correctly' do
+ subject.refresh_cache
+
+ # It does not update total issues cache
+ expect(Rails.cache.read(get_cache_key(subject, project_1))).to eq(nil)
+ expect(Rails.cache.read(get_cache_key(subject, project_2))).to eq(nil)
+
+ expect(Rails.cache.read(get_cache_key(subject, project_1, true))).to eq(1)
+ expect(Rails.cache.read(get_cache_key(subject, project_1, true))).to eq(1)
+ end
+ end
+
+ context 'when issues count is already cached' do
+ before do
+ create(:issue, project: project_2)
+ subject.refresh_cache
+ end
+
+ it 'does update cache again' do
+ expect(Rails.cache).not_to receive(:write)
+
+ subject.refresh_cache
+ end
+ end
+ end
+
+ def get_cache_key(subject, project, public_key = false)
+ service = subject.count_service.new(project)
+
+ if public_key
+ service.cache_key(service.class::PUBLIC_COUNT_KEY)
+ else
+ service.cache_key(service.class::TOTAL_COUNT_KEY)
+ end
+ end
+end
diff --git a/spec/services/projects/open_issues_count_service_spec.rb b/spec/services/projects/open_issues_count_service_spec.rb
index 06b470849b3..562c14a8df8 100644
--- a/spec/services/projects/open_issues_count_service_spec.rb
+++ b/spec/services/projects/open_issues_count_service_spec.rb
@@ -50,5 +50,40 @@ describe Projects::OpenIssuesCountService do
end
end
end
+
+ context '#refresh_cache', :use_clean_rails_memory_store_caching do
+ let(:subject) { described_class.new(project) }
+
+ before do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+ end
+
+ context 'when cache is empty' do
+ it 'refreshes cache keys correctly' do
+ subject.refresh_cache
+
+ expect(Rails.cache.read(subject.cache_key(described_class::PUBLIC_COUNT_KEY))).to eq(2)
+ expect(Rails.cache.read(subject.cache_key(described_class::TOTAL_COUNT_KEY))).to eq(3)
+ end
+ end
+
+ context 'when cache is outdated' do
+ before do
+ subject.refresh_cache
+ end
+
+ it 'refreshes cache keys correctly' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+
+ subject.refresh_cache
+
+ expect(Rails.cache.read(subject.cache_key(described_class::PUBLIC_COUNT_KEY))).to eq(3)
+ expect(Rails.cache.read(subject.cache_key(described_class::TOTAL_COUNT_KEY))).to eq(5)
+ end
+ end
+ end
end
end
diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb
index 723cb374c37..5c2e79ff9af 100644
--- a/spec/services/projects/update_remote_mirror_service_spec.rb
+++ b/spec/services/projects/update_remote_mirror_service_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Projects::UpdateRemoteMirrorService do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
+ let(:owner) { project.owner }
let(:remote_project) { create(:forked_project_with_submodules) }
let(:repository) { project.repository }
let(:raw_repository) { repository.raw }
@@ -9,13 +10,11 @@ describe Projects::UpdateRemoteMirrorService do
subject { described_class.new(project, project.creator) }
- describe "#execute", :skip_gitaly_mock do
+ describe "#execute" do
before do
- create_branch(repository, 'existing-branch')
- allow(raw_repository).to receive(:remote_tags) do
- generate_tags(repository, 'v1.0.0', 'v1.1.0')
- end
- allow(raw_repository).to receive(:push_remote_branches).and_return(true)
+ repository.add_branch(owner, 'existing-branch', 'master')
+
+ allow(remote_mirror).to receive(:update_repository).and_return(true)
end
it "fetches the remote repository" do
@@ -34,307 +33,57 @@ describe Projects::UpdateRemoteMirrorService do
expect(result[:status]).to eq(:success)
end
- describe 'Syncing branches' do
+ context 'when syncing all branches' do
it "push all the branches the first time" do
allow(repository).to receive(:fetch_remote)
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, local_branch_names)
-
- subject.execute(remote_mirror)
- end
-
- it "does not push anything is remote is up to date" do
- allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
-
- expect(raw_repository).not_to receive(:push_remote_branches)
-
- subject.execute(remote_mirror)
- end
-
- it "sync new branches" do
- # call local_branch_names early so it is not called after the new branch has been created
- current_branches = local_branch_names
- allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, current_branches) }
- create_branch(repository, 'my-new-branch')
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['my-new-branch'])
-
- subject.execute(remote_mirror)
- end
-
- it "sync updated branches" do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_branch(repository, 'existing-branch')
- end
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
+ expect(remote_mirror).to receive(:update_repository).with({})
subject.execute(remote_mirror)
end
-
- context 'when push only protected branches option is set' do
- let(:unprotected_branch_name) { 'existing-branch' }
- let(:protected_branch_name) do
- project.repository.branch_names.find { |n| n != unprotected_branch_name }
- end
- let!(:protected_branch) do
- create(:protected_branch, project: project, name: protected_branch_name)
- end
-
- before do
- project.reload
- remote_mirror.only_protected_branches = true
- end
-
- it "sync updated protected branches" do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_branch(repository, protected_branch_name)
- end
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
-
- subject.execute(remote_mirror)
- end
-
- it 'does not sync unprotected branches' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_branch(repository, unprotected_branch_name)
- end
-
- expect(raw_repository).not_to receive(:push_remote_branches).with(remote_mirror.remote_name, [unprotected_branch_name])
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when branch exists in local and remote repo' do
- context 'when it has diverged' do
- it 'syncs branches' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_remote_branch(repository, remote_mirror.remote_name, 'markdown')
- end
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['markdown'])
-
- subject.execute(remote_mirror)
- end
- end
- end
-
- describe 'for delete' do
- context 'when branch exists in local and remote repo' do
- it 'deletes the branch from remote repo' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- delete_branch(repository, 'existing-branch')
- end
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when push only protected branches option is set' do
- before do
- remote_mirror.only_protected_branches = true
- end
-
- context 'when branch exists in local and remote repo' do
- let!(:protected_branch_name) { local_branch_names.first }
-
- before do
- create(:protected_branch, project: project, name: protected_branch_name)
- project.reload
- end
-
- it 'deletes the protected branch from remote repo' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- delete_branch(repository, protected_branch_name)
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
-
- subject.execute(remote_mirror)
- end
-
- it 'does not delete the unprotected branch from remote repo' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- delete_branch(repository, 'existing-branch')
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when branch only exists on remote repo' do
- let!(:protected_branch_name) { 'remote-branch' }
-
- before do
- create(:protected_branch, project: project, name: protected_branch_name)
- end
-
- context 'when it has diverged' do
- it 'does not delete the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- rev = repository.find_branch('markdown').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches)
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when it has not diverged' do
- it 'deletes the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- masterrev = repository.find_branch('master').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, protected_branch_name, masterrev.id)
- end
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
-
- subject.execute(remote_mirror)
- end
- end
- end
- end
-
- context 'when branch only exists on remote repo' do
- context 'when it has diverged' do
- it 'does not delete the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- rev = repository.find_branch('markdown').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches)
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when it has not diverged' do
- it 'deletes the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- masterrev = repository.find_branch('master').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', masterrev.id)
- end
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['remote-branch'])
-
- subject.execute(remote_mirror)
- end
- end
- end
- end
end
- describe 'Syncing tags' do
- before do
- allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
+ context 'when only syncing protected branches' do
+ let(:unprotected_branch_name) { 'existing-branch' }
+ let(:protected_branch_name) do
+ project.repository.branch_names.find { |n| n != unprotected_branch_name }
end
-
- context 'when there are not tags to push' do
- it 'does not try to push tags' do
- allow(repository).to receive(:remote_tags) { {} }
- allow(repository).to receive(:tags) { [] }
-
- expect(repository).not_to receive(:push_tags)
-
- subject.execute(remote_mirror)
- end
+ let!(:protected_branch) do
+ create(:protected_branch, project: project, name: protected_branch_name)
end
- context 'when there are some tags to push' do
- it 'pushes tags to remote' do
- allow(raw_repository).to receive(:remote_tags) { {} }
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['v1.0.0', 'v1.1.0'])
-
- subject.execute(remote_mirror)
- end
+ before do
+ project.reload
+ remote_mirror.only_protected_branches = true
end
- context 'when there are some tags to delete' do
- it 'deletes tags from remote' do
- remote_tags = generate_tags(repository, 'v1.0.0', 'v1.1.0')
- allow(raw_repository).to receive(:remote_tags) { remote_tags }
-
- repository.rm_tag(create(:user), 'v1.0.0')
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['v1.0.0'])
+ it "sync updated protected branches" do
+ allow(repository).to receive(:fetch_remote)
+ expect(remote_mirror).to receive(:update_repository).with(only_branches_matching: [protected_branch_name])
- subject.execute(remote_mirror)
- end
+ subject.execute(remote_mirror)
end
end
end
- def create_branch(repository, branch_name)
- rugged = repository.rugged
- masterrev = repository.find_branch('master').dereferenced_target
- parentrev = repository.commit(masterrev).parent_id
-
- rugged.references.create("refs/heads/#{branch_name}", parentrev)
-
- repository.expire_branches_cache
- end
-
- def create_remote_branch(repository, remote_name, branch_name, source_id)
- rugged = repository.rugged
-
- rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_id)
- end
-
def sync_remote(repository, remote_name, local_branch_names)
- rugged = repository.rugged
-
local_branch_names.each do |branch|
- target = repository.find_branch(branch).try(:dereferenced_target)
- rugged.references.create("refs/remotes/#{remote_name}/#{branch}", target.id) if target
+ commit = repository.commit(branch)
+ repository.write_ref("refs/remotes/#{remote_name}/#{branch}", commit.id) if commit
end
end
def update_remote_branch(repository, remote_name, branch)
- rugged = repository.rugged
- masterrev = repository.find_branch('master').dereferenced_target.id
+ masterrev = repository.commit('master').id
- rugged.references.create("refs/remotes/#{remote_name}/#{branch}", masterrev, force: true)
+ repository.write_ref("refs/remotes/#{remote_name}/#{branch}", masterrev, force: true)
repository.expire_branches_cache
end
def update_branch(repository, branch)
- rugged = repository.rugged
- masterrev = repository.find_branch('master').dereferenced_target.id
-
- # Updated existing branch
- rugged.references.create("refs/heads/#{branch}", masterrev, force: true)
- repository.expire_branches_cache
- end
-
- def delete_branch(repository, branch)
- rugged = repository.rugged
+ masterrev = repository.commit('master').id
- rugged.references.delete("refs/heads/#{branch}")
+ repository.write_ref("refs/heads/#{branch}", masterrev, force: true)
repository.expire_branches_cache
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 76f1e625fda..f82d4b483e7 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -19,7 +19,9 @@ describe Users::DestroyService do
end
it 'will delete the project' do
- expect_any_instance_of(Projects::DestroyService).to receive(:execute).once
+ expect_next_instance_of(Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).once
+ end
service.execute(user)
end
@@ -32,7 +34,9 @@ describe Users::DestroyService do
end
it 'destroys a project in pending_delete' do
- expect_any_instance_of(Projects::DestroyService).to receive(:execute).once
+ expect_next_instance_of(Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).once
+ end
service.execute(user)
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 7995f2c9ae7..622e56e1da5 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -60,6 +60,36 @@ describe WebHookService do
).once
end
+ context 'when auth credentials are present' do
+ let(:url) {'https://example.org'}
+ let(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') }
+
+ it 'uses the credentials' do
+ WebMock.stub_request(:post, url)
+
+ service_instance.execute
+
+ expect(WebMock).to have_requested(:post, url).with(
+ headers: headers.merge('Authorization' => 'Basic ZGVtbzpkZW1v')
+ ).once
+ end
+ end
+
+ context 'when auth credentials are partial present' do
+ let(:url) {'https://example.org'}
+ let(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') }
+
+ it 'uses the credentials anyways' do
+ WebMock.stub_request(:post, url)
+
+ service_instance.execute
+
+ expect(WebMock).to have_requested(:post, url).with(
+ headers: headers.merge('Authorization' => 'Basic ZGVtbzo=')
+ ).once
+ end
+ end
+
it 'catches exceptions' do
WebMock.stub_request(:post, project_hook.url).to_raise(StandardError.new('Some error'))
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index dac609e2545..fdce8e84620 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -69,6 +69,7 @@ RSpec.configure do |config|
config.include StubFeatureFlags
config.include StubGitlabCalls
config.include StubGitlabData
+ config.include ExpectNextInstanceOf
config.include TestEnv
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::IntegrationHelpers, type: :feature
diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb
index b4c71d69119..89a5518239d 100644
--- a/spec/support/features/reportable_note_shared_examples.rb
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -22,7 +22,7 @@ shared_examples 'reportable note' do |type|
expect(dropdown).to have_link('Report as abuse', href: abuse_report_path)
- if type == 'issue'
+ if type == 'issue' || type == 'merge_request'
expect(dropdown).to have_button('Delete comment')
else
expect(dropdown).to have_link('Delete comment', href: note_url(note, project))
diff --git a/spec/support/helpers/expect_next_instance_of.rb b/spec/support/helpers/expect_next_instance_of.rb
new file mode 100644
index 00000000000..b95046b2b42
--- /dev/null
+++ b/spec/support/helpers/expect_next_instance_of.rb
@@ -0,0 +1,13 @@
+module ExpectNextInstanceOf
+ def expect_next_instance_of(klass, *new_args)
+ receive_new = receive(:new)
+ receive_new.with(*new_args) if new_args.any?
+
+ expect(klass).to receive_new
+ .and_wrap_original do |method, *original_args|
+ method.call(*original_args).tap do |instance|
+ yield(instance)
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 329f18cd288..87cfb6c04dc 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -46,8 +46,8 @@ module LoginHelpers
@current_user = user
end
- def gitlab_sign_in_via(provider, user, uid)
- mock_auth_hash(provider, uid, user.email)
+ def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
+ mock_auth_hash(provider, uid, user.email, saml_response)
visit new_user_session_path
click_link provider
end
@@ -87,7 +87,7 @@ module LoginHelpers
click_link "oauth-login-#{provider}"
end
- def mock_auth_hash(provider, uid, email)
+ def mock_auth_hash(provider, uid, email, saml_response = nil)
# The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing.
OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
@@ -109,12 +109,21 @@ module LoginHelpers
email: email,
image: 'mock_user_thumbnail_url'
}
+ },
+ response_object: {
+ document: saml_xml(saml_response)
}
}
})
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
end
+ def saml_xml(raw_saml_response)
+ return '' if raw_saml_response.blank?
+
+ XMLSecurity::SignedDocument.new(raw_saml_response, [])
+ end
+
def mock_saml_config
OpenStruct.new(name: 'saml', label: 'saml', args: {
assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback',
@@ -125,6 +134,14 @@ module LoginHelpers
})
end
+ def mock_saml_config_with_upstream_two_factor_authn_contexts
+ config = mock_saml_config
+ config.args[:upstream_two_factor_authn_contexts] = %w(urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN)
+ config
+ end
+
def stub_omniauth_provider(provider, context: Rails.application)
env = env_from_context(context)
@@ -140,13 +157,16 @@ module LoginHelpers
env['omniauth.error.strategy'] = strategy
end
- def stub_omniauth_saml_config(messages)
- set_devise_mapping(context: Rails.application)
- Rails.application.routes.disable_clear_and_finalize = true
- Rails.application.routes.draw do
+ def stub_omniauth_saml_config(messages, context: Rails.application)
+ set_devise_mapping(context: context)
+ routes = Rails.application.routes
+ routes.disable_clear_and_finalize = true
+ routes.formatter.clear
+ routes.draw do
post '/users/auth/saml' => 'omniauth_callbacks#saml'
end
- allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config)
+ saml_config = messages.key?(:providers) ? messages[:providers].first : mock_saml_config
+ allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: saml_config)
stub_omniauth_setting(messages)
stub_saml_authorize_path_helpers
end
diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb
index c98aa503ed1..3b49d0b3319 100644
--- a/spec/support/helpers/merge_request_diff_helpers.rb
+++ b/spec/support/helpers/merge_request_diff_helpers.rb
@@ -2,7 +2,7 @@ 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', visible: false).send_keys(:return)
+ line[:num].find('.js-add-diff-note-button', visible: false).send_keys(:return)
end
def get_line_components(line_holder, diff_side = nil)
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index bceaf8277ee..471b0a74a19 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -15,9 +15,14 @@ module StubObjectStorage
return unless enabled
+ stub_object_storage(connection_params: uploader.object_store_credentials,
+ remote_directory: remote_directory)
+ end
+
+ def stub_object_storage(connection_params:, remote_directory:)
Fog.mock!
- ::Fog::Storage.new(uploader.object_store_credentials).tap do |connection|
+ ::Fog::Storage.new(connection_params).tap do |connection|
begin
connection.directories.create(key: remote_directory)
rescue Excon::Error::Conflict
diff --git a/spec/support/matchers/match_ids.rb b/spec/support/matchers/match_ids.rb
index d8424405b96..1cb6b74acac 100644
--- a/spec/support/matchers/match_ids.rb
+++ b/spec/support/matchers/match_ids.rb
@@ -10,6 +10,13 @@ RSpec::Matchers.define :match_ids do |*expected|
'matches elements by ids'
end
+ failure_message do
+ actual_ids = map_ids(actual)
+ expected_ids = map_ids(expected)
+
+ "expected IDs #{actual_ids} in:\n\n #{actual.inspect}\n\nto match IDs #{expected_ids} in:\n\n #{expected.inspect}"
+ end
+
def map_ids(elements)
elements = elements.flatten if elements.respond_to?(:flatten)
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index 8676f895a83..e650a176041 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -65,6 +65,14 @@ RSpec.shared_examples "redis_shared_examples" do
end
describe '.url' do
+ it 'withstands mutation' do
+ url1 = described_class.url
+ url2 = described_class.url
+ url1 << 'foobar' unless url1.frozen?
+
+ expect(url2).not_to end_with('foobar')
+ end
+
context 'when yml file with env variable' do
let(:config_file_name) { config_with_environment_variable_inside }
@@ -101,7 +109,6 @@ RSpec.shared_examples "redis_shared_examples" do
before do
clear_pool
end
-
after do
clear_pool
end
diff --git a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
index 639b0924197..64c3b80136d 100644
--- a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
+++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
@@ -18,7 +18,7 @@ shared_examples 'project features apply to issuables' do |klass|
before do
_ = issuable
- gitlab_sign_in(user) if user
+ sign_in(user) if user
visit path
end
diff --git a/spec/support/shared_examples/serializers/note_entity_examples.rb b/spec/support/shared_examples/serializers/note_entity_examples.rb
index 9097c8e5513..ec208aba2a9 100644
--- a/spec/support/shared_examples/serializers/note_entity_examples.rb
+++ b/spec/support/shared_examples/serializers/note_entity_examples.rb
@@ -3,8 +3,8 @@ shared_examples 'note entity' do
context 'basic note' do
it 'exposes correct elements' do
- expect(subject).to include(:type, :author, :note, :note_html, :current_user,
- :discussion_id, :emoji_awardable, :award_emoji, :report_abuse_path, :attachment)
+ expect(subject).to include(:type, :author, :note, :note_html, :current_user, :discussion_id,
+ :emoji_awardable, :award_emoji, :report_abuse_path, :attachment, :noteable_note_url, :resolvable)
end
it 'does not expose elements for specific notes cases' do
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index c7f5694ff43..7e673681c31 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -191,6 +191,18 @@ describe ObjectStorage do
it "calls a cache path" do
expect { |b| uploader.use_file(&b) }.to yield_with_args(%r[tmp/cache])
end
+
+ it "cleans up the cached file" do
+ cached_path = ''
+
+ uploader.use_file do |path|
+ cached_path = path
+
+ expect(File.exist?(cached_path)).to be_truthy
+ end
+
+ expect(File.exist?(cached_path)).to be_falsey
+ end
end
end
diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
index 0870b8f09f9..66c064e3fba 100644
--- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -6,6 +6,7 @@ describe 'devise/shared/_signin_box' do
stub_devise
assign(:ldap_servers, [])
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+ allow(view).to receive(:captcha_enabled?).and_return(false)
end
it 'is shown when Crowd is enabled' do
diff --git a/spec/workers/delete_diff_files_worker_spec.rb b/spec/workers/delete_diff_files_worker_spec.rb
new file mode 100644
index 00000000000..e0edd313922
--- /dev/null
+++ b/spec/workers/delete_diff_files_worker_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe DeleteDiffFilesWorker do
+ describe '#perform' do
+ let(:merge_request) { create(:merge_request) }
+ let(:merge_request_diff) { merge_request.merge_request_diff }
+
+ it 'deletes all merge request diff files' do
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to change { merge_request_diff.merge_request_diff_files.count }
+ .from(20).to(0)
+ end
+
+ it 'updates state to without_files' do
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to change { merge_request_diff.reload.state }
+ .from('collected').to('without_files')
+ end
+
+ it 'does nothing if diff was already marked as "without_files"' do
+ merge_request_diff.clean!
+
+ expect_any_instance_of(MergeRequestDiff).not_to receive(:clean!)
+
+ described_class.new.perform(merge_request_diff.id)
+ end
+
+ it 'rollsback if something goes wrong' do
+ expect(MergeRequestDiffFile).to receive_message_chain(:where, :delete_all)
+ .and_raise
+
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to raise_error
+
+ merge_request_diff.reload
+
+ expect(merge_request_diff.state).to eq('collected')
+ expect(merge_request_diff.merge_request_diff_files.count).to eq(20)
+ end
+ end
+end
diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb
index 36594515005..06d9e125105 100644
--- a/spec/workers/delete_user_worker_spec.rb
+++ b/spec/workers/delete_user_worker_spec.rb
@@ -5,15 +5,17 @@ describe DeleteUserWorker do
let!(:current_user) { create(:user) }
it "calls the DeleteUserWorker with the params it was given" do
- expect_any_instance_of(Users::DestroyService).to receive(:execute)
- .with(user, {})
+ expect_next_instance_of(Users::DestroyService) do |service|
+ expect(service).to receive(:execute).with(user, {})
+ end
described_class.new.perform(current_user.id, user.id)
end
it "uses symbolized keys" do
- expect_any_instance_of(Users::DestroyService).to receive(:execute)
- .with(user, test: "test")
+ expect_next_instance_of(Users::DestroyService) do |service|
+ expect(service).to receive(:execute).with(user, test: "test")
+ end
described_class.new.perform(current_user.id, user.id, "test" => "test")
end
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
index 80137815d2b..0b553db0ca4 100644
--- a/spec/workers/update_merge_requests_worker_spec.rb
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -18,13 +18,9 @@ describe UpdateMergeRequestsWorker do
end
it 'executes MergeRequests::RefreshService with expected values' do
- expect(MergeRequests::RefreshService).to receive(:new)
- .with(project, user).and_wrap_original do |method, *args|
- method.call(*args).tap do |refresh_service|
- expect(refresh_service)
- .to receive(:execute).with(oldrev, newrev, ref)
- end
- end
+ expect_next_instance_of(MergeRequests::RefreshService, project, user) do |refresh_service|
+ expect(refresh_service).to receive(:execute).with(oldrev, newrev, ref)
+ end
perform
end
diff --git a/yarn.lock b/yarn.lock
index cefd7c9a62e..ef7fa659d6e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -78,9 +78,9 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.23.0":
- version "1.23.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.23.0.tgz#42047aeedcc06bc12d417ed1efadad1749af9670"
+"@gitlab-org/gitlab-svgs@^1.24.0":
+ version "1.24.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.24.0.tgz#3b2b58c5a1d58ce784f486d648bd87cbbb06cedc"
"@sindresorhus/is@^0.7.0":
version "0.7.0"
@@ -297,13 +297,6 @@ ajv-keywords@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be"
-ajv@^4.9.1:
- version "4.11.8"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
- dependencies:
- co "^4.6.0"
- json-stable-stringify "^1.0.1"
-
ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
@@ -1300,12 +1293,6 @@ blob@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
-block-stream@*:
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
- dependencies:
- inherits "~2.0.0"
-
bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.4.6, bluebird@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
@@ -2365,7 +2352,7 @@ de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
-debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6:
+debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
@@ -3423,6 +3410,12 @@ fs-access@^1.0.0:
dependencies:
null-check "^1.0.0"
+fs-minipass@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
+ dependencies:
+ minipass "^2.2.1"
+
fs-write-stream-atomic@^1.0.8:
version "1.0.10"
resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
@@ -3437,28 +3430,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
fsevents@^1.0.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
- dependencies:
- nan "^2.3.0"
- node-pre-gyp "^0.6.39"
-
-fstream-ignore@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
dependencies:
- fstream "^1.0.0"
- inherits "2"
- minimatch "^3.0.0"
-
-fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
- version "1.0.11"
- resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
- dependencies:
- graceful-fs "^4.1.2"
- inherits "~2.0.0"
- mkdirp ">=0.5 0"
- rimraf "2"
+ nan "^2.9.2"
+ node-pre-gyp "^0.10.0"
ftp@~0.3.10:
version "0.3.10"
@@ -3690,10 +3666,6 @@ handlebars@^4.0.1, handlebars@^4.0.3:
optionalDependencies:
uglify-js "^2.6"
-har-schema@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
-
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@@ -3707,13 +3679,6 @@ har-validator@~2.0.6:
is-my-json-valid "^2.12.4"
pinkie-promise "^2.0.0"
-har-validator@~4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
- dependencies:
- ajv "^4.9.1"
- har-schema "^1.0.5"
-
har-validator@~5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
@@ -3816,7 +3781,7 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.0"
-hawk@3.1.3, hawk@~3.1.3:
+hawk@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
dependencies:
@@ -3988,6 +3953,12 @@ iconv-lite@0.4.19, iconv-lite@^0.4.17:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+iconv-lite@^0.4.4:
+ version "0.4.23"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
icss-replace-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
@@ -4010,6 +3981,12 @@ ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
+ignore-walk@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
+ dependencies:
+ minimatch "^3.0.4"
+
ignore@^3.3.3, ignore@^3.3.7:
version "3.3.8"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.8.tgz#3f8e9c35d38708a3a7e0e9abb6c73e7ee7707b2b"
@@ -4069,7 +4046,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@@ -4657,12 +4634,6 @@ json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
-json-stable-stringify@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
- dependencies:
- jsonify "~0.0.0"
-
json-stringify-safe@5.0.x, json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@@ -4675,10 +4646,6 @@ json5@^0.5.0, json5@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
-jsonify@~0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
-
jsonpointer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
@@ -5238,7 +5205,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
-"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+"minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
@@ -5256,6 +5223,19 @@ minimist@~0.0.1:
version "0.0.10"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+minipass@^2.2.1, minipass@^2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233"
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
+minizlib@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
+ dependencies:
+ minipass "^2.2.1"
+
mississippi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
@@ -5278,7 +5258,7 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
-mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
@@ -5334,9 +5314,9 @@ mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
-nan@^2.3.0:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
+nan@^2.9.2:
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
nanomatch@^1.2.9:
version "1.2.9"
@@ -5359,6 +5339,14 @@ natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+needle@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d"
+ dependencies:
+ debug "^2.1.2"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
@@ -5407,21 +5395,20 @@ node-forge@0.6.33:
util "^0.10.3"
vm-browserify "0.0.4"
-node-pre-gyp@^0.6.39:
- version "0.6.39"
- resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649"
+node-pre-gyp@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz#6e4ef5bb5c5203c6552448828c852c40111aac46"
dependencies:
detect-libc "^1.0.2"
- hawk "3.1.3"
mkdirp "^0.5.1"
+ needle "^2.2.0"
nopt "^4.0.1"
+ npm-packlist "^1.1.6"
npmlog "^4.0.2"
rc "^1.1.7"
- request "2.81.0"
rimraf "^2.6.1"
semver "^5.3.0"
- tar "^2.2.1"
- tar-pack "^3.4.0"
+ tar "^4"
node-uuid@~1.4.7:
version "1.4.8"
@@ -5546,6 +5533,17 @@ normalize-url@^1.4.0:
query-string "^4.1.0"
sort-keys "^1.0.0"
+npm-bundled@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308"
+
+npm-packlist@^1.1.6:
+ version "1.1.10"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a"
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -5630,7 +5628,7 @@ on-headers@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
-once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
+once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
@@ -5905,10 +5903,6 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
-performance-now@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
-
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -6370,10 +6364,6 @@ qs@~6.2.0:
version "6.2.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.3.tgz#1cfcb25c10a9b2b483053ff39f5dfc9233908cfe"
-qs@~6.4.0:
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
-
query-string@^4.1.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.2.tgz#ec0fd765f58a50031a3968c2431386f8947a5cdd"
@@ -6491,7 +6481,7 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2"
path-type "^2.0.0"
-"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3:
+"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3:
version "2.3.4"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071"
dependencies:
@@ -6697,33 +6687,6 @@ request@2.75.x:
tough-cookie "~2.3.0"
tunnel-agent "~0.4.1"
-request@2.81.0:
- version "2.81.0"
- resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
- dependencies:
- aws-sign2 "~0.6.0"
- aws4 "^1.2.1"
- caseless "~0.12.0"
- combined-stream "~1.0.5"
- extend "~3.0.0"
- forever-agent "~0.6.1"
- form-data "~2.1.1"
- har-validator "~4.2.1"
- hawk "~3.1.3"
- http-signature "~1.1.0"
- is-typedarray "~1.0.0"
- isstream "~0.1.2"
- json-stringify-safe "~5.0.1"
- mime-types "~2.1.7"
- oauth-sign "~0.8.1"
- performance-now "^0.2.0"
- qs "~6.4.0"
- safe-buffer "^5.0.1"
- stringstream "~0.0.4"
- tough-cookie "~2.3.0"
- tunnel-agent "^0.6.0"
- uuid "^3.0.0"
-
request@^2.0.0, request@^2.74.0:
version "2.83.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
@@ -6830,7 +6793,7 @@ right-align@^0.1.1:
dependencies:
align-text "^0.1.1"
-rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2:
+rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
dependencies:
@@ -6875,12 +6838,20 @@ safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+safe-buffer@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+
safe-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
dependencies:
ret "~0.1.10"
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+
sanitize-html@^1.16.1:
version "1.16.3"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.16.3.tgz#96c1b44a36ff7312e1c22a14b05274370ac8bd56"
@@ -6893,6 +6864,10 @@ sanitize-html@^1.16.1:
srcset "^1.0.0"
xtend "^4.0.0"
+sax@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+
sax@~1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"
@@ -7549,26 +7524,17 @@ tapable@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
-tar-pack@^3.4.0:
- version "3.4.1"
- resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f"
- dependencies:
- debug "^2.2.0"
- fstream "^1.0.10"
- fstream-ignore "^1.0.5"
- once "^1.3.3"
- readable-stream "^2.1.4"
- rimraf "^2.5.1"
- tar "^2.2.1"
- uid-number "^0.0.6"
-
-tar@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+tar@^4:
+ version "4.4.4"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.4.tgz#ec8409fae9f665a4355cc3b4087d0820232bb8cd"
dependencies:
- block-stream "*"
- fstream "^1.0.2"
- inherits "2"
+ chownr "^1.0.1"
+ fs-minipass "^1.2.5"
+ minipass "^2.3.3"
+ minizlib "^1.1.0"
+ mkdirp "^0.5.0"
+ safe-buffer "^5.1.2"
+ yallist "^3.0.2"
term-size@^1.2.0:
version "1.2.0"
@@ -7793,10 +7759,6 @@ uglifyjs-webpack-plugin@^1.2.4:
webpack-sources "^1.1.0"
worker-farm "^1.5.2"
-uid-number@^0.0.6:
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
-
ultron@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
@@ -7975,7 +7937,7 @@ utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
-uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0:
+uuid@^3.0.1, uuid@^3.1.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
@@ -8386,6 +8348,10 @@ yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+yallist@^3.0.0, yallist@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
+
yargs-parser@^9.0.2:
version "9.0.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"