summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke "Jared" Bennett <lbennett@gitlab.com>2017-04-24 15:11:36 +0100
committerLuke "Jared" Bennett <lbennett@gitlab.com>2017-04-24 15:11:36 +0100
commit56ab571fd3b4c589e040b40cf9bd81044b5d8ee3 (patch)
tree2d106526d9a80ef446198a33b2265b74e53d50d6
parentf7723b8ebdc437feb731275d9a3cc109a6ce1761 (diff)
parentc768026474b9dff9f6f988372e4eefb85b1d8be9 (diff)
downloadgitlab-ce-20827-getting-started-update-new-group-view.tar.gz
Merge branch 'master' into 20827-getting-started-update-new-group-view20827-getting-started-update-new-group-view
-rw-r--r--.eslintrc8
-rw-r--r--.gitlab-ci.yml21
-rw-r--r--.gitlab/issue_templates/Bug.md8
-rw-r--r--CHANGELOG.md266
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock5
-rw-r--r--PROCESS.md13
-rw-r--r--README.md2
-rw-r--r--VERSION2
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_canceled.ico (renamed from app/assets/images/ci_favicons/icon_status_canceled.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_created.ico (renamed from app/assets/images/ci_favicons/icon_status_created.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_failed.ico (renamed from app/assets/images/ci_favicons/icon_status_failed.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_manual.ico (renamed from app/assets/images/ci_favicons/icon_status_manual.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_not_found.ico (renamed from app/assets/images/ci_favicons/icon_status_not_found.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_pending.ico (renamed from app/assets/images/ci_favicons/icon_status_pending.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_running.ico (renamed from app/assets/images/ci_favicons/icon_status_running.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_skipped.ico (renamed from app/assets/images/ci_favicons/icon_status_skipped.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_success.ico (renamed from app/assets/images/ci_favicons/icon_status_success.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_warning.ico (renamed from app/assets/images/ci_favicons/icon_status_warning.ico)bin5430 -> 5430 bytes
-rw-r--r--app/assets/javascripts/awards_handler.js100
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js1
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js2
-rw-r--r--app/assets/javascripts/blob/blob_fork_suggestion.js58
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js4
-rw-r--r--app/assets/javascripts/boards/components/board.js168
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js28
-rw-r--r--app/assets/javascripts/boards/components/board_list.js9
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js112
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js246
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js120
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js126
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js130
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js280
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js268
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js102
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js86
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js123
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js98
-rw-r--r--app/assets/javascripts/boards/mixins/modal_mixins.js22
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js60
-rw-r--r--app/assets/javascripts/boards/models/list.js11
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js219
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js156
-rw-r--r--app/assets/javascripts/build.js26
-rw-r--r--app/assets/javascripts/ci_status_icons.js34
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js33
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.js81
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_issue_component.js85
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js85
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_production_component.js85
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.js103
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js85
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js85
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.js41
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_service.js64
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js171
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js100
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js260
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js310
-rw-r--r--app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js42
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js202
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js36
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js96
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js50
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js110
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js90
-rw-r--r--app/assets/javascripts/dispatcher.js25
-rw-r--r--app/assets/javascripts/droplab/constants.js2
-rw-r--r--app/assets/javascripts/droplab/drop_down.js3
-rw-r--r--app/assets/javascripts/dropzone_input.js24
-rw-r--r--app/assets/javascripts/due_date_select.js11
-rw-r--r--app/assets/javascripts/environments/components/environment.js2
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js97
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue103
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js30
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue33
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue (renamed from app/assets/javascripts/environments/components/environment_item.js)250
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.js31
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue33
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue (renamed from app/assets/javascripts/environments/components/environment_rollback.js)39
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue (renamed from app/assets/javascripts/environments/components/environment_stop.js)35
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue (renamed from app/assets/javascripts/environments/components/environment_terminal_button.js)22
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js97
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue117
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js122
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js76
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js106
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js298
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js194
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js298
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js778
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js168
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js100
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js2
-rw-r--r--app/assets/javascripts/groups_select.js6
-rw-r--r--app/assets/javascripts/issuable_form.js3
-rw-r--r--app/assets/javascripts/issue.js108
-rw-r--r--app/assets/javascripts/issue_show/index.js36
-rw-r--r--app/assets/javascripts/issue_show/issue_title.vue (renamed from app/assets/javascripts/issue_show/issue_title.js)32
-rw-r--r--app/assets/javascripts/labels_select.js8
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js19
-rw-r--r--app/assets/javascripts/lib/utils/constants.js2
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js12
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js346
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js171
-rw-r--r--app/assets/javascripts/main.js9
-rw-r--r--app/assets/javascripts/member_expiration_date.js3
-rw-r--r--app/assets/javascripts/merge_request_widget.js8
-rw-r--r--app/assets/javascripts/merged_buttons.js12
-rw-r--r--app/assets/javascripts/milestone_select.js3
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js2
-rw-r--r--app/assets/javascripts/notes.js22
-rw-r--r--app/assets/javascripts/pipelines/components/async_button.vue (renamed from app/assets/javascripts/vue_pipelines_index/components/async_button.js)55
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue34
-rw-r--r--app/assets/javascripts/pipelines/components/error_state.vue21
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.js (renamed from app/assets/javascripts/vue_pipelines_index/components/nav_controls.js)0
-rw-r--r--app/assets/javascripts/pipelines/components/navigation_tabs.js (renamed from app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js)0
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.js (renamed from app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js)0
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.js (renamed from app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js)3
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.js (renamed from app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js)0
-rw-r--r--app/assets/javascripts/pipelines/components/stage.js (renamed from app/assets/javascripts/vue_pipelines_index/components/stage.js)28
-rw-r--r--app/assets/javascripts/pipelines/components/status.js (renamed from app/assets/javascripts/vue_pipelines_index/components/status.js)0
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.js (renamed from app/assets/javascripts/vue_pipelines_index/components/time_ago.js)0
-rw-r--r--app/assets/javascripts/pipelines/event_hub.js (renamed from app/assets/javascripts/vue_pipelines_index/event_hub.js)0
-rw-r--r--app/assets/javascripts/pipelines/index.js (renamed from app/assets/javascripts/vue_pipelines_index/index.js)0
-rw-r--r--app/assets/javascripts/pipelines/pipelines.js (renamed from app/assets/javascripts/vue_pipelines_index/pipelines.js)4
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js (renamed from app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js)0
-rw-r--r--app/assets/javascripts/pipelines/stores/pipelines_store.js (renamed from app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js)0
-rw-r--r--app/assets/javascripts/shortcuts.js7
-rw-r--r--app/assets/javascripts/shortcuts_wiki.js16
-rw-r--r--app/assets/javascripts/usage_ping.js15
-rw-r--r--app/assets/javascripts/user_callout.js2
-rw-r--r--app/assets/javascripts/user_tabs.js22
-rw-r--r--app/assets/javascripts/users_select.js3
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/empty_state.js33
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/error_state.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js14
-rw-r--r--app/assets/stylesheets/framework/animations.scss14
-rw-r--r--app/assets/stylesheets/framework/awards.scss20
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/calendar.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss75
-rw-r--r--app/assets/stylesheets/framework/filters.scss28
-rw-r--r--app/assets/stylesheets/framework/header.scss13
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss30
-rw-r--r--app/assets/stylesheets/framework/variables.scss41
-rw-r--r--app/assets/stylesheets/pages/builds.scss19
-rw-r--r--app/assets/stylesheets/pages/commits.scss7
-rw-r--r--app/assets/stylesheets/pages/diff.scss4
-rw-r--r--app/assets/stylesheets/pages/events.scss22
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss3
-rw-r--r--app/assets/stylesheets/pages/notes.scss53
-rw-r--r--app/assets/stylesheets/pages/profile.scss59
-rw-r--r--app/assets/stylesheets/pages/projects.scss32
-rw-r--r--app/assets/stylesheets/pages/search.scss15
-rw-r--r--app/controllers/admin/application_settings_controller.rb13
-rw-r--r--app/controllers/admin/cohorts_controller.rb11
-rw-r--r--app/controllers/admin/groups_controller.rb8
-rw-r--r--app/controllers/admin/spam_logs_controller.rb2
-rw-r--r--app/controllers/concerns/creates_commit.rb62
-rw-r--r--app/controllers/concerns/membership_actions.rb42
-rw-r--r--app/controllers/groups/group_members_controller.rb24
-rw-r--r--app/controllers/projects/application_controller.rb5
-rw-r--r--app/controllers/projects/blob_controller.rb18
-rw-r--r--app/controllers/projects/commit_controller.rb10
-rw-r--r--app/controllers/projects/compare_controller.rb1
-rw-r--r--app/controllers/projects/git_http_controller.rb6
-rw-r--r--app/controllers/projects/hooks_controller.rb4
-rwxr-xr-xapp/controllers/projects/merge_requests_controller.rb78
-rw-r--r--app/controllers/projects/project_members_controller.rb24
-rw-r--r--app/controllers/projects/tree_controller.rb6
-rw-r--r--app/controllers/projects_controller.rb10
-rw-r--r--app/controllers/registrations_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb5
-rw-r--r--app/helpers/blob_helper.rb44
-rw-r--r--app/helpers/diff_helper.rb2
-rw-r--r--app/helpers/events_helper.rb28
-rw-r--r--app/helpers/issuables_helper.rb11
-rw-r--r--app/helpers/issues_helper.rb8
-rw-r--r--app/helpers/javascript_helper.rb5
-rw-r--r--app/helpers/notes_helper.rb19
-rw-r--r--app/helpers/preferences_helper.rb6
-rw-r--r--app/helpers/projects_helper.rb23
-rw-r--r--app/helpers/snippets_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb8
-rw-r--r--app/helpers/submodule_helper.rb12
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/helpers/webpack_helper.rb30
-rw-r--r--app/models/abuse_report.rb2
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/blob.rb30
-rw-r--r--app/models/ci/trigger.rb2
-rw-r--r--app/models/commit.rb15
-rw-r--r--app/models/concerns/cache_markdown_field.rb126
-rw-r--r--app/models/concerns/discussion_on_diff.rb12
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/note_on_diff.rb14
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/container_repository.rb7
-rw-r--r--app/models/diff_discussion.rb1
-rw-r--r--app/models/diff_note.rb14
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/identity.rb2
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/label.rb6
-rw-r--r--app/models/legacy_diff_discussion.rb8
-rw-r--r--app/models/legacy_diff_note.rb3
-rw-r--r--app/models/member.rb28
-rw-r--r--app/models/members/group_member.rb12
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/merge_request.rb9
-rw-r--r--app/models/merge_request_diff.rb10
-rw-r--r--app/models/note.rb11
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_services/chat_notification_service.rb2
-rw-r--r--app/models/project_team.rb4
-rw-r--r--app/models/repository.rb28
-rw-r--r--app/models/spam_log.rb4
-rw-r--r--app/models/user.rb19
-rw-r--r--app/policies/group_policy.rb1
-rw-r--r--app/serializers/cohort_activity_month_entity.rb11
-rw-r--r--app/serializers/cohort_entity.rb17
-rw-r--r--app/serializers/cohorts_entity.rb4
-rw-r--r--app/serializers/cohorts_serializer.rb3
-rw-r--r--app/serializers/status_entity.rb6
-rw-r--r--app/services/cohorts_service.rb100
-rw-r--r--app/services/commits/change_service.rb52
-rw-r--r--app/services/commits/cherry_pick_service.rb2
-rw-r--r--app/services/commits/create_service.rb74
-rw-r--r--app/services/commits/revert_service.rb2
-rw-r--r--app/services/delete_merged_branches_service.rb11
-rw-r--r--app/services/event_create_service.rb2
-rw-r--r--app/services/files/base_service.rb80
-rw-r--r--app/services/files/create_dir_service.rb15
-rw-r--r--app/services/files/create_service.rb36
-rw-r--r--app/services/files/delete_service.rb (renamed from app/services/files/destroy_service.rb)6
-rw-r--r--app/services/files/multi_service.rb125
-rw-r--r--app/services/files/update_service.rb30
-rw-r--r--app/services/members/authorized_destroy_service.rb24
-rw-r--r--app/services/members/create_service.rb8
-rw-r--r--app/services/projects/create_service.rb3
-rw-r--r--app/services/projects/import_service.rb1
-rw-r--r--app/services/search/global_service.rb11
-rw-r--r--app/services/search/group_service.rb18
-rw-r--r--app/services/search_service.rb2
-rw-r--r--app/services/users/activity_service.rb22
-rw-r--r--app/services/users/build_service.rb100
-rw-r--r--app/services/users/create_service.rb95
-rw-r--r--app/services/users/destroy_service.rb2
-rw-r--r--app/services/users/migrate_to_ghost_user_service.rb34
-rw-r--r--app/services/validate_new_branch_service.rb5
-rw-r--r--app/views/admin/application_settings/_form.html.haml16
-rw-r--r--app/views/admin/cohorts/_cohorts_table.html.haml28
-rw-r--r--app/views/admin/cohorts/_usage_ping.html.haml10
-rw-r--r--app/views/admin/cohorts/index.html.haml16
-rw-r--r--app/views/admin/dashboard/_head.html.haml4
-rw-r--r--app/views/admin/health_check/show.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/admin/services/index.html.haml2
-rw-r--r--app/views/award_emoji/_awards_block.html.haml4
-rw-r--r--app/views/discussions/_discussion.html.haml13
-rw-r--r--app/views/events/event/_common.html.haml11
-rw-r--r--app/views/events/event/_created_project.html.haml3
-rw-r--r--app/views/events/event/_note.html.haml3
-rw-r--r--app/views/events/event/_push.html.haml6
-rw-r--r--app/views/groups/subgroups.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml8
-rw-r--r--app/views/help/ui.html.haml8
-rw-r--r--app/views/layouts/_head.html.haml6
-rw-r--r--app/views/layouts/header/_default.html.haml9
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml4
-rw-r--r--app/views/layouts/nav/_explore.html.haml19
-rw-r--r--app/views/layouts/nav/_project.html.haml8
-rw-r--r--app/views/notify/project_was_exported_email.html.haml2
-rw-r--r--app/views/projects/blob/_blob.html.haml15
-rw-r--r--app/views/projects/blob/_header.html.haml14
-rw-r--r--app/views/projects/blob/_image.html.haml15
-rw-r--r--app/views/projects/blob/_markup.html.haml4
-rw-r--r--app/views/projects/blob/_svg.html.haml9
-rw-r--r--app/views/projects/blob/_text.html.haml21
-rw-r--r--app/views/projects/blob/_too_large.html.haml5
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml16
-rw-r--r--app/views/projects/builds/_sidebar.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml10
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/diffs/_content.html.haml2
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml3
-rw-r--r--app/views/projects/diffs/_text_file.html.haml3
-rw-r--r--app/views/projects/environments/metrics.html.haml3
-rw-r--r--app/views/projects/issues/show.html.haml1
-rw-r--r--app/views/projects/labels/edit.html.haml2
-rw-r--r--app/views/projects/labels/index.html.haml2
-rw-r--r--app/views/projects/labels/new.html.haml2
-rw-r--r--app/views/projects/merge_requests/_head.html.haml21
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/index.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_pipelines.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml11
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_error.html.haml6
-rw-r--r--app/views/projects/milestones/edit.html.haml4
-rw-r--r--app/views/projects/milestones/index.html.haml2
-rw-r--r--app/views/projects/milestones/new.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml4
-rw-r--r--app/views/projects/new.html.haml5
-rw-r--r--app/views/projects/notes/_comment_button.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml36
-rw-r--r--app/views/projects/pipelines/index.html.haml2
-rw-r--r--app/views/projects/registry/repositories/_image.html.haml2
-rw-r--r--app/views/projects/registry/repositories/_tag.html.haml2
-rw-r--r--app/views/projects/show.html.haml10
-rw-r--r--app/views/projects/snippets/edit.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/triggers/_form.html.haml2
-rw-r--r--app/views/projects/variables/_table.html.haml2
-rw-r--r--app/views/projects/wikis/_main_links.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml5
-rw-r--r--app/views/search/results/_merge_request.html.haml9
-rw-r--r--app/views/shared/_branch_switcher.html.haml6
-rw-r--r--app/views/shared/_import_form.html.haml2
-rw-r--r--app/views/shared/_mr_head.html.haml4
-rw-r--r--app/views/shared/_new_commit_form.html.haml6
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml3
-rw-r--r--app/views/shared/_user_callout.html.haml17
-rw-r--r--app/views/shared/empty_states/_issues.html.haml6
-rw-r--r--app/views/shared/groups/_group.html.haml3
-rw-r--r--app/views/shared/icons/_icon_arrow_circle_o_right.svg2
-rw-r--r--app/views/shared/icons/_icon_merged.svg2
-rw-r--r--app/views/shared/icons/_icon_trash_o.svg (renamed from app/views/shared/icons/_trash_o.svg)0
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml15
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml4
-rw-r--r--app/views/shared/projects/_project.html.haml9
-rw-r--r--app/views/snippets/edit.html.haml2
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/views/users/show.html.haml10
-rw-r--r--app/workers/build_coverage_worker.rb3
-rw-r--r--app/workers/clear_database_cache_worker.rb24
-rw-r--r--app/workers/gitlab_usage_ping_worker.rb31
-rw-r--r--app/workers/schedule_update_user_activity_worker.rb10
-rw-r--r--app/workers/system_hook_worker.rb2
-rw-r--r--app/workers/update_user_activity_worker.rb26
-rw-r--r--changelogs/unreleased/12818-ci-status-as-favicon.yml4
-rw-r--r--changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml5
-rw-r--r--changelogs/unreleased/17325-rugged-gem-update.yml4
-rw-r--r--changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml4
-rw-r--r--changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml4
-rw-r--r--changelogs/unreleased/20841-getting-started-better-empty-state-for-merge-requests-view.yml4
-rw-r--r--changelogs/unreleased/20914-project-home-width.yml4
-rw-r--r--changelogs/unreleased/21451-allow-disable-mr-link.yml4
-rw-r--r--changelogs/unreleased/22303-symbolic-in-tree.yml4
-rw-r--r--changelogs/unreleased/22714-update-all-instances-of-fa-refresh.yml4
-rw-r--r--changelogs/unreleased/23363-use-strong-params-in-wikis-controller.yml4
-rw-r--r--changelogs/unreleased/23655-api-group-issues.yml4
-rw-r--r--changelogs/unreleased/23674-simplify-milestone-summary.yml4
-rw-r--r--changelogs/unreleased/23862-fix-group-project-count.yml4
-rw-r--r--changelogs/unreleased/24137-issuable-permalink.yml4
-rw-r--r--changelogs/unreleased/24166-close-builds-dropdown.yml4
-rw-r--r--changelogs/unreleased/24187-set-git-terminal-prompt-env-var-in-initializer.yml4
-rw-r--r--changelogs/unreleased/24215-closed-issues-board.yml4
-rw-r--r--changelogs/unreleased/24240-add-monitoring-endpoints.yml4
-rw-r--r--changelogs/unreleased/24421-personal-milestone-count-badges.yml4
-rw-r--r--changelogs/unreleased/24501-new-file-existing-branch.yml4
-rw-r--r--changelogs/unreleased/24784-system-notes-meta-data.yml4
-rw-r--r--changelogs/unreleased/24861-stringify-group-member-details.yml4
-rw-r--r--changelogs/unreleased/25188-polyfill-es-symbol.yml4
-rw-r--r--changelogs/unreleased/25332-make-file-templates-easy-to-use-and-discover.yml4
-rw-r--r--changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml4
-rw-r--r--changelogs/unreleased/25556-prevent-users-from-disconnecting-gitlab-account-from-cas.yml4
-rw-r--r--changelogs/unreleased/26188-tag-creation-404-for-guests.yml4
-rw-r--r--changelogs/unreleased/26202-change-dropdown-style-slightly.yml4
-rw-r--r--changelogs/unreleased/26208-animate-drodowns.yml4
-rw-r--r--changelogs/unreleased/26236-monospace-gfm.yml4
-rw-r--r--changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml4
-rw-r--r--changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml4
-rw-r--r--changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml4
-rw-r--r--changelogs/unreleased/27174-filter-filters.yml4
-rw-r--r--changelogs/unreleased/27262-issue-recent-searches.yml4
-rw-r--r--changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml4
-rw-r--r--changelogs/unreleased/27293-remove-repeated-labels.yml4
-rw-r--r--changelogs/unreleased/27503-feature-status-aria-labels.yml4
-rw-r--r--changelogs/unreleased/27574-pipelines-empty-state.yml4
-rw-r--r--changelogs/unreleased/27580-fix-show-go-back.yml4
-rw-r--r--changelogs/unreleased/27655-clear-emoji-search-after-selection.yml4
-rw-r--r--changelogs/unreleased/27729-improve-webpack-dev-environment.yml4
-rw-r--r--changelogs/unreleased/27878-new-service-for-creating-user.yml4
-rw-r--r--changelogs/unreleased/27910-admin-can-create-project-in-all-groups.yml4
-rw-r--r--changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml5
-rw-r--r--changelogs/unreleased/28017-separate-ce-params-on-api.yml4
-rw-r--r--changelogs/unreleased/28030-infinite-offset.yml4
-rw-r--r--changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml4
-rw-r--r--changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml4
-rw-r--r--changelogs/unreleased/28424-labels-support-color-names-in-backend.yml4
-rw-r--r--changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml4
-rw-r--r--changelogs/unreleased/28574-jira-trigers.yml4
-rw-r--r--changelogs/unreleased/28575-expand-collapse-look.yml4
-rw-r--r--changelogs/unreleased/28614-harmonious-color-palette.yml4
-rw-r--r--changelogs/unreleased/28634-todos-margin.yml4
-rw-r--r--changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml4
-rw-r--r--changelogs/unreleased/28695-move-all-associated-records-to-ghost-user.yml4
-rw-r--r--changelogs/unreleased/28713-fe-style-guide.yml4
-rw-r--r--changelogs/unreleased/28732-expandable-folders.yml4
-rw-r--r--changelogs/unreleased/28799-todo-creation.yml4
-rw-r--r--changelogs/unreleased/28810-projectfinder-should-handle-more-options.yml4
-rw-r--r--changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml4
-rw-r--r--changelogs/unreleased/28899-linking-to-edit-file.yml5
-rw-r--r--changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml4
-rw-r--r--changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml4
-rw-r--r--changelogs/unreleased/29043-upgrade-vue-and-remove-warnings.yml4
-rw-r--r--changelogs/unreleased/29046-fix-github-importer-open-prs.yml4
-rw-r--r--changelogs/unreleased/29056-backport-ee-cleanup-database-file.yml4
-rw-r--r--changelogs/unreleased/29116-maxint-error.yml4
-rw-r--r--changelogs/unreleased/29128-profile-page-icons.yml4
-rw-r--r--changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml4
-rw-r--r--changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml4
-rw-r--r--changelogs/unreleased/29189-discussion-button.yml4
-rw-r--r--changelogs/unreleased/29209-sign-up-form-name.yml4
-rw-r--r--changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml4
-rw-r--r--changelogs/unreleased/29341-add-metrics-button-env-overview.yml4
-rw-r--r--changelogs/unreleased/29364-private-projects-mr-fix.yml4
-rw-r--r--changelogs/unreleased/29405-fix-project-wiki-update.yml4
-rw-r--r--changelogs/unreleased/29414-fix-toggle-discussion-link-jump.yml4
-rw-r--r--changelogs/unreleased/29428-new-directory-from-existing-branch.yml4
-rw-r--r--changelogs/unreleased/29432-prevent-click-disabled-btn.yml4
-rw-r--r--changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml4
-rw-r--r--changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml4
-rw-r--r--changelogs/unreleased/29483-spam-check-only-title-and-description.yml4
-rw-r--r--changelogs/unreleased/29492-useless-queries.yml4
-rw-r--r--changelogs/unreleased/29550-fix-quick-submit-on-preview.yml4
-rw-r--r--changelogs/unreleased/29555-align-all-todo.yml4
-rw-r--r--changelogs/unreleased/29575-polling.yml4
-rw-r--r--changelogs/unreleased/29595-customize-experience-callout.yml4
-rw-r--r--changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml4
-rw-r--r--changelogs/unreleased/29669-redirect-referer-params.yml4
-rw-r--r--changelogs/unreleased/29670-jira-integration-documentation-improvment.yml4
-rw-r--r--changelogs/unreleased/29734-prometheus-monitoring-page-displays-button-to-control-manual-actions.yml4
-rw-r--r--changelogs/unreleased/29801-add-slash-slack-commands-to-api-doc.yml5
-rw-r--r--changelogs/unreleased/29816-create-keyboard-shortcut-for-editing-wiki-page.yml4
-rw-r--r--changelogs/unreleased/29828-change-search-hint-in-new-filters.yml4
-rw-r--r--changelogs/unreleased/29830-build-scroll-indicator.yml4
-rw-r--r--changelogs/unreleased/29843-project-subgroup-transfer.yml4
-rw-r--r--changelogs/unreleased/29866-navbar-counters.yml4
-rw-r--r--changelogs/unreleased/2989-run-cicd-pipelines-on-a-schedule-idea1-basic-backend-implementation.yml4
-rw-r--r--changelogs/unreleased/29897-remove-force-scroll-for-mr-changes-diff.yml4
-rw-r--r--changelogs/unreleased/29929-jira-doc.yml4
-rw-r--r--changelogs/unreleased/29930-fix-profile-cover-button-a11y.yml4
-rw-r--r--changelogs/unreleased/29950-vue-pagination-icons.yml4
-rw-r--r--changelogs/unreleased/29977-style-comments-and-system-notes-real-time-updates.yml4
-rw-r--r--changelogs/unreleased/30021-api-deploy_keys-can_push-is-not-honoured.yml4
-rw-r--r--changelogs/unreleased/30024-owner-can-t-initialize-git-repo-for-new-project-in-group.yml4
-rw-r--r--changelogs/unreleased/30056-rename-milestones-empty.yml4
-rw-r--r--changelogs/unreleased/30112-fix-pipelines-sub-nav-highlight.yml4
-rw-r--r--changelogs/unreleased/30125-markdown-security.yml4
-rw-r--r--changelogs/unreleased/30195-document-search-param-on-api.yml4
-rw-r--r--changelogs/unreleased/30291-reopen-mr.yml4
-rw-r--r--changelogs/unreleased/30306-transaction-while-moving-issues-to-ghost-user.yml4
-rw-r--r--changelogs/unreleased/30349-create-users-build-service.yml4
-rw-r--r--changelogs/unreleased/30400-fix-blob-highlighting-in-search.yml4
-rw-r--r--changelogs/unreleased/30484-profile-dropdown-account-name.yml4
-rw-r--r--changelogs/unreleased/30493-env-deploy-tooltip.yml5
-rw-r--r--changelogs/unreleased/30587-pipeline-icon-z.yml4
-rw-r--r--changelogs/unreleased/30588-fix-javascript-sourcemaps-w-chrome-breakpoints.yml4
-rw-r--r--changelogs/unreleased/30672-versioned-markdown-cache.yml4
-rw-r--r--changelogs/unreleased/30678-improve-dev-server-process.yml4
-rw-r--r--changelogs/unreleased/31009-disable-test-settings-on-services-when-repository-is-empty.yml4
-rw-r--r--changelogs/unreleased/31193-ff-copy.yml4
-rw-r--r--changelogs/unreleased/4195-add-sorting-to-project-milestones.yml4
-rw-r--r--changelogs/unreleased/6260-frontend-prevent-authored-votes.yml4
-rw-r--r--changelogs/unreleased/8998_skip_pending_commits_if_not_head.yml4
-rw-r--r--changelogs/unreleased/adam-finish-5993-closed-issuable.yml4
-rw-r--r--changelogs/unreleased/adam-prevent-two-issue-trackers.yml4
-rw-r--r--changelogs/unreleased/add-blob-copy-button.yml4
-rw-r--r--changelogs/unreleased/add-dimension-etag-caching-metrics.yml4
-rw-r--r--changelogs/unreleased/add-error-empty-states.yml4
-rw-r--r--changelogs/unreleased/add-field-for-group-name.yml4
-rw-r--r--changelogs/unreleased/add-issue-modal-loading-indicator.yml4
-rw-r--r--changelogs/unreleased/add-labels-to-issue-hook.yml4
-rw-r--r--changelogs/unreleased/add-test-backoff-util.yml4
-rw-r--r--changelogs/unreleased/add-todos-shortcut.yml4
-rw-r--r--changelogs/unreleased/add-ui-for-trigger-schedule.yml4
-rw-r--r--changelogs/unreleased/add-vue-loader.yml4
-rw-r--r--changelogs/unreleased/add_quick_submit_for_snippets_form.yml4
-rw-r--r--changelogs/unreleased/add_remove_concurrent_index_to_database_helper.yml4
-rw-r--r--changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml4
-rw-r--r--changelogs/unreleased/award-emoji-button-smiley-animation.yml4
-rw-r--r--changelogs/unreleased/bb_save_trace.yml5
-rw-r--r--changelogs/unreleased/boards-done-add-tooltip.yml4
-rw-r--r--changelogs/unreleased/bug-api_milestone_merge_requests_scope.yml4
-rw-r--r--changelogs/unreleased/bugfix-systemhook.yml4
-rw-r--r--changelogs/unreleased/button-capitalization.yml4
-rw-r--r--changelogs/unreleased/calendar-tooltips.yml4
-rw-r--r--changelogs/unreleased/chore-23493-remaining-time-tooltip.yml5
-rw-r--r--changelogs/unreleased/clean_carrierwave_tempfiles.yml4
-rw-r--r--changelogs/unreleased/cleaner-additional-award-emoji-button.yml4
-rw-r--r--changelogs/unreleased/create-collapsed-todo-button.yml5
-rw-r--r--changelogs/unreleased/dm-copy-code-as-gfm.yml4
-rw-r--r--changelogs/unreleased/dm-copy-diff-file-title-as-gfm.yml4
-rw-r--r--changelogs/unreleased/dm-link-discussion-to-outdated-diff.yml4
-rw-r--r--changelogs/unreleased/dz-cleanup-add-users.yml4
-rw-r--r--changelogs/unreleased/dz-fix-group-move.yml4
-rw-r--r--changelogs/unreleased/dz-hide-zero-counter.yml4
-rw-r--r--changelogs/unreleased/dz-refactor-admin-group-members.yml4
-rw-r--r--changelogs/unreleased/dz-refactor-create-members.yml4
-rw-r--r--changelogs/unreleased/dz-remove-repo-version.yml4
-rw-r--r--changelogs/unreleased/emoji-menu-duplicated-search-icon.yml4
-rw-r--r--changelogs/unreleased/empty-task-list-alignment.yml4
-rw-r--r--changelogs/unreleased/enable-snippets-by-default.yml4
-rw-r--r--changelogs/unreleased/environment-performance-improvements.yml4
-rw-r--r--changelogs/unreleased/es6-class-issue.yml4
-rw-r--r--changelogs/unreleased/feature-custom-lfs.yml4
-rw-r--r--changelogs/unreleased/feature-enforce-2fa-per-group.yml4
-rw-r--r--changelogs/unreleased/feature-gh-rake-task.yml4
-rw-r--r--changelogs/unreleased/feature-multi-level-container-registry-images.yml4
-rw-r--r--changelogs/unreleased/feature-tokens-rake-task.yml4
-rw-r--r--changelogs/unreleased/feature-use-gitaly-for-commit-is-ancestor.yml4
-rw-r--r--changelogs/unreleased/feature-use-gitaly-for-commit-show.yml4
-rw-r--r--changelogs/unreleased/file-import-export-path-disclosure.yml5
-rw-r--r--changelogs/unreleased/fix-29093.yml4
-rw-r--r--changelogs/unreleased/fix-admin-projects.yml4
-rw-r--r--changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml4
-rw-r--r--changelogs/unreleased/fix-gb-remove-deprecated-pipeline-processing-code.yml4
-rw-r--r--changelogs/unreleased/fix-gh-import-status-check.yml4
-rw-r--r--changelogs/unreleased/fix-github-importer-slowness.yml4
-rw-r--r--changelogs/unreleased/fix-groups-long-url.yml4
-rw-r--r--changelogs/unreleased/fix-import-fork.yml4
-rw-r--r--changelogs/unreleased/fix-import-namespace.yml4
-rw-r--r--changelogs/unreleased/fix-issue-23237.yml4
-rw-r--r--changelogs/unreleased/fix-link-prometheus-opening-outside-gitlab.yml4
-rw-r--r--changelogs/unreleased/fix-milestone-name-on-show.yml4
-rw-r--r--changelogs/unreleased/fix-user-profile-tabs-showing-raw-json-instead.yml5
-rw-r--r--changelogs/unreleased/fix_admin_monitoring_background.yml4
-rw-r--r--changelogs/unreleased/fix_cache_expiration_in_repository.yml4
-rw-r--r--changelogs/unreleased/fix_link_in_readme.yml4
-rw-r--r--changelogs/unreleased/fix_rake_gitlab_check_sidekiq.yml4
-rw-r--r--changelogs/unreleased/fix_spaces_in_label_title.yml4
-rw-r--r--changelogs/unreleased/fix_updated_field_in_issues-atom.yml4
-rw-r--r--changelogs/unreleased/fix_visibility_level.yml4
-rw-r--r--changelogs/unreleased/fix_wiki_commit_message.yml4
-rw-r--r--changelogs/unreleased/fl-remove-ujs-pipelines.yml4
-rw-r--r--changelogs/unreleased/form-focus-previous-incorrect-form.yml4
-rw-r--r--changelogs/unreleased/gitaly-refs.yml4
-rw-r--r--changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml4
-rw-r--r--changelogs/unreleased/handle-failure-when-deleting-tags.yml4
-rw-r--r--changelogs/unreleased/introduce-polling-interval-multiplier.yml4
-rw-r--r--changelogs/unreleased/issue-boards-cant-drag-fix.yml4
-rw-r--r--changelogs/unreleased/issue-boards-new-search-bar.yml4
-rw-r--r--changelogs/unreleased/issue_27212.yml4
-rw-r--r--changelogs/unreleased/issue_29449.yml4
-rw-r--r--changelogs/unreleased/issue_91_ee_backport.yml4
-rw-r--r--changelogs/unreleased/jej-group-name-disclosure.yml4
-rw-r--r--changelogs/unreleased/make-karma-fast-again.yml4
-rw-r--r--changelogs/unreleased/make_user_mentions_case_insensitive.yml4
-rw-r--r--changelogs/unreleased/menu-shortcut.yml4
-rw-r--r--changelogs/unreleased/metrics-button-misplaced.yml4
-rw-r--r--changelogs/unreleased/microsoft-teams-integration.yml4
-rw-r--r--changelogs/unreleased/milestone-not-showing-correctly-title.yml4
-rw-r--r--changelogs/unreleased/move-search-labels.yml4
-rw-r--r--changelogs/unreleased/mr-diffs-speed-up.yml4
-rw-r--r--changelogs/unreleased/mr-new-page-changing-url.yml4
-rw-r--r--changelogs/unreleased/namespace-race-condition.yml4
-rw-r--r--changelogs/unreleased/new-resolvable-discussion.yml4
-rw-r--r--changelogs/unreleased/omnibus-gitlab-1993-check-shell-repositories-path-group-is-root.yml4
-rw-r--r--changelogs/unreleased/open-redirect-continue-params.yml4
-rw-r--r--changelogs/unreleased/open-redirect-host-field.yml4
-rw-r--r--changelogs/unreleased/optimise-builds-view.yml4
-rw-r--r--changelogs/unreleased/option-to-be-notified-of-own-activity.yml4
-rw-r--r--changelogs/unreleased/pages-debug-log.yml4
-rw-r--r--changelogs/unreleased/pipeline-tooltips-overflow.yml4
-rw-r--r--changelogs/unreleased/pipelines-build-tooltip.yml4
-rw-r--r--changelogs/unreleased/projects-list-line-breaks.yml4
-rw-r--r--changelogs/unreleased/query-users-by-extern-uid.yml4
-rw-r--r--changelogs/unreleased/quiet-pipelines.yml5
-rw-r--r--changelogs/unreleased/refresh-permissions-recent-users.yml4
-rw-r--r--changelogs/unreleased/remember-me-missasligned-mobile.yml4
-rw-r--r--changelogs/unreleased/remove-double-newline-for-single-attachments.yml4
-rw-r--r--changelogs/unreleased/remove_index_for_users-current_sign_in_at.yml4
-rw-r--r--changelogs/unreleased/remove_is_admin.yml4
-rw-r--r--changelogs/unreleased/rename_all_issues.yml4
-rw-r--r--changelogs/unreleased/rename_done_to_closed.yml4
-rw-r--r--changelogs/unreleased/replace_closing_mr_icon.yml4
-rw-r--r--changelogs/unreleased/reset-new-branch-button.yml4
-rw-r--r--changelogs/unreleased/right-sidebar-closed-default-mobile.yml4
-rw-r--r--changelogs/unreleased/scrollable-secondary-tabs.yml4
-rw-r--r--changelogs/unreleased/sh-fix-ssh-keys-with-spaces.yml4
-rw-r--r--changelogs/unreleased/sh-relax-wiki-slug-constraint.yml4
-rw-r--r--changelogs/unreleased/sh-remove-tags-from-explore.yml4
-rw-r--r--changelogs/unreleased/simplify-docs-trigger.yml4
-rw-r--r--changelogs/unreleased/spec_for_schema.yml4
-rw-r--r--changelogs/unreleased/style-proc-cop.yml4
-rw-r--r--changelogs/unreleased/submodules-no-dotgit.yml4
-rw-r--r--changelogs/unreleased/tc-fix-pipeline-recipient.yml4
-rw-r--r--changelogs/unreleased/tc-fix-unplayable-build-action-404.yml4
-rw-r--r--changelogs/unreleased/tc-pipeline-show-trigger-date.yml4
-rw-r--r--changelogs/unreleased/tc-show-pipeline-coverage-if-avail.yml4
-rw-r--r--changelogs/unreleased/time-tracking-color-not-consistent.yml4
-rw-r--r--changelogs/unreleased/uassign_on_member_removing.yml4
-rw-r--r--changelogs/unreleased/update-issue-board-cards-design.yml4
-rw-r--r--changelogs/unreleased/update-test-bundle-ignored-files.yml4
-rw-r--r--changelogs/unreleased/use-corejs-polyfills.yml4
-rw-r--r--changelogs/unreleased/use-hashie-forbidden_attributes.yml4
-rw-r--r--changelogs/unreleased/user-activity-scroll-bar.yml4
-rw-r--r--changelogs/unreleased/user-callout-showing-on-all-profiles.yml4
-rw-r--r--changelogs/unreleased/user-profile-join-date.yml4
-rw-r--r--changelogs/unreleased/zj-chat-notification-default-branch.yml4
-rw-r--r--changelogs/unreleased/zj-kube-service-auto-fill.yml4
-rw-r--r--config/dependency_decisions.yml54
-rw-r--r--config/environments/test.rb7
-rw-r--r--config/gitlab.yml.example4
-rw-r--r--config/initializers/1_settings.rb30
-rw-r--r--config/routes/admin.rb3
-rw-r--r--config/routes/project.rb32
-rw-r--r--config/routes/repository.rb139
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--config/webpack.config.js39
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb6
-rw-r--r--db/fixtures/development/20_nested_groups.rb68
-rw-r--r--db/migrate/20130218141258_convert_closed_to_state_in_issue.rb2
-rw-r--r--db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb2
-rw-r--r--db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb2
-rw-r--r--db/migrate/20130315124931_user_color_scheme.rb2
-rw-r--r--db/migrate/20131112220935_add_visibility_level_to_projects.rb2
-rw-r--r--db/migrate/20140313092127_migrate_already_imported_projects.rb2
-rw-r--r--db/migrate/20141007100818_add_visibility_level_to_snippet.rb2
-rw-r--r--db/migrate/20151209144329_migrate_ci_web_hooks.rb2
-rw-r--r--db/migrate/20151209145909_migrate_ci_emails.rb2
-rw-r--r--db/migrate/20151210125232_migrate_ci_slack_service.rb2
-rw-r--r--db/migrate/20151210125927_migrate_ci_hip_chat_service.rb2
-rw-r--r--db/migrate/20160713222618_add_usage_ping_to_application_settings.rb9
-rw-r--r--db/migrate/20161007073613_create_user_activities.rb7
-rw-r--r--db/migrate/20170307125949_add_last_activity_on_to_users.rb9
-rw-r--r--db/migrate/20170328010804_add_uuid_to_application_settings.rb16
-rw-r--r--db/migrate/20170407135259_add_foreigh_key_trigger_requests_trigger.rb15
-rw-r--r--db/migrate/20170410133135_add_version_field_to_markdown_cache.rb25
-rw-r--r--db/migrate/20170418103908_delete_orphan_notification_settings.rb24
-rw-r--r--db/migrate/20170419001229_add_index_to_system_note_metadata.rb17
-rw-r--r--db/post_migrate/20161128170531_drop_user_activities_table.rb9
-rw-r--r--db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb1
-rw-r--r--db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb87
-rw-r--r--db/post_migrate/20170406142253_migrate_user_project_view.rb19
-rw-r--r--db/schema.rb22
-rw-r--r--doc/README.md7
-rw-r--r--doc/administration/high_availability/redis.md2
-rw-r--r--doc/administration/integration/plantuml.md6
-rw-r--r--doc/administration/polling.md24
-rw-r--r--doc/api/README.md12
-rw-r--r--doc/api/access_requests.md8
-rw-r--r--doc/api/award_emoji.md18
-rw-r--r--doc/api/boards.md12
-rw-r--r--doc/api/branches.md14
-rw-r--r--doc/api/build_variables.md10
-rw-r--r--doc/api/commits.md18
-rw-r--r--doc/api/deploy_keys.md10
-rw-r--r--doc/api/deployments.md4
-rw-r--r--doc/api/enviroments.md10
-rw-r--r--doc/api/groups.md6
-rw-r--r--doc/api/issues.md30
-rw-r--r--doc/api/jobs.md22
-rw-r--r--doc/api/labels.md12
-rw-r--r--doc/api/members.md10
-rw-r--r--doc/api/merge_requests.md40
-rw-r--r--doc/api/milestones.md14
-rw-r--r--doc/api/notes.md30
-rw-r--r--doc/api/pipeline_triggers.md12
-rw-r--r--doc/api/pipelines.md12
-rw-r--r--doc/api/project_snippets.md12
-rw-r--r--doc/api/projects.md50
-rw-r--r--doc/api/repositories.md12
-rw-r--r--doc/api/runners.md6
-rw-r--r--doc/api/services.md79
-rw-r--r--doc/api/tags.md12
-rw-r--r--doc/api/users.md69
-rw-r--r--doc/articles/index.md16
-rw-r--r--doc/ci/README.md5
-rw-r--r--doc/ci/autodeploy/img/auto_deploy_dropdown.pngbin44380 -> 99422 bytes
-rw-r--r--doc/ci/autodeploy/index.md34
-rw-r--r--doc/ci/img/pipelines.pngbin7516 -> 6298 bytes
-rw-r--r--doc/ci/triggers/README.md28
-rw-r--r--doc/ci/triggers/img/trigger_schedule_create.pngbin0 -> 34264 bytes
-rw-r--r--doc/ci/triggers/img/trigger_schedule_edit.pngbin0 -> 18524 bytes
-rw-r--r--doc/ci/triggers/img/trigger_schedule_updated_next_run_at.pngbin0 -> 21896 bytes
-rw-r--r--doc/development/doc_styleguide.md7
-rw-r--r--doc/development/fe_guide/droplab/droplab.md256
-rw-r--r--doc/development/fe_guide/droplab/plugins/ajax.md37
-rw-r--r--doc/development/fe_guide/droplab/plugins/filter.md45
-rw-r--r--doc/development/fe_guide/droplab/plugins/input_setter.md60
-rw-r--r--doc/development/fe_guide/img/boards_diagram.pngbin0 -> 30538 bytes
-rw-r--r--doc/development/fe_guide/img/vue_arch.pngbin0 -> 9848 bytes
-rw-r--r--doc/development/fe_guide/index.md63
-rw-r--r--doc/development/fe_guide/performance.md8
-rw-r--r--doc/development/fe_guide/style_guide_js.md50
-rw-r--r--doc/development/fe_guide/testing.md56
-rw-r--r--doc/development/fe_guide/vue.md355
-rw-r--r--doc/development/migration_style_guide.md133
-rw-r--r--doc/development/polling.md1
-rw-r--r--doc/development/testing.md429
-rw-r--r--doc/development/what_requires_downtime.md237
-rw-r--r--doc/development/writing_documentation.md4
-rw-r--r--doc/install/README.md4
-rw-r--r--doc/install/digitaloceandocker.md5
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/intro/README.md2
-rw-r--r--doc/raketasks/backup_restore.md10
-rw-r--r--doc/topics/authentication/index.md46
-rw-r--r--doc/topics/git/index.md61
-rw-r--r--doc/topics/index.md8
-rw-r--r--doc/university/glossary/README.md2
-rw-r--r--doc/update/9.0-to-9.1.md4
-rw-r--r--doc/update/README.md14
-rw-r--r--doc/update/patch_versions.md2
-rw-r--r--doc/user/admin_area/img/cohorts.pngbin0 -> 439635 bytes
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md102
-rw-r--r--doc/user/admin_area/user_cohorts.md37
-rw-r--r--doc/user/discussions/img/btn_new_issue_for_all_discussions.png (renamed from doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png)bin29007 -> 29007 bytes
-rw-r--r--doc/user/discussions/img/comment_type_toggle.gifbin0 -> 70796 bytes
-rw-r--r--doc/user/discussions/img/discussion_comment.pngbin0 -> 57189 bytes
-rw-r--r--doc/user/discussions/img/discussion_view.png (renamed from doc/user/project/merge_requests/img/discussion_view.png)bin73821 -> 73821 bytes
-rw-r--r--doc/user/discussions/img/discussions_resolved.png (renamed from doc/user/project/merge_requests/img/discussions_resolved.png)bin4152 -> 4152 bytes
-rw-r--r--doc/user/discussions/img/new_issue_for_discussion.png (renamed from doc/user/project/merge_requests/img/new_issue_for_discussion.png)bin39563 -> 39563 bytes
-rw-r--r--doc/user/discussions/img/only_allow_merge_if_all_discussions_are_resolved.png (renamed from doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png)bin17888 -> 17888 bytes
-rw-r--r--doc/user/discussions/img/only_allow_merge_if_all_discussions_are_resolved_msg.png (renamed from doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png)bin4962 -> 4962 bytes
-rw-r--r--doc/user/discussions/img/preview_issue_for_discussion.png (renamed from doc/user/project/merge_requests/img/preview_issue_for_discussion.png)bin82412 -> 82412 bytes
-rw-r--r--doc/user/discussions/img/preview_issue_for_discussions.png (renamed from doc/user/project/merge_requests/img/preview_issue_for_discussions.png)bin143871 -> 143871 bytes
-rw-r--r--doc/user/discussions/img/resolve_comment_button.png (renamed from doc/user/project/merge_requests/img/resolve_comment_button.png)bin4722 -> 4722 bytes
-rw-r--r--doc/user/discussions/img/resolve_discussion_button.png (renamed from doc/user/project/merge_requests/img/resolve_discussion_button.png)bin4683 -> 4683 bytes
-rw-r--r--doc/user/discussions/img/resolve_discussion_issue_notice.png (renamed from doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png)bin10307 -> 10307 bytes
-rw-r--r--doc/user/discussions/img/resolve_discussion_open_issue.png (renamed from doc/user/project/merge_requests/img/resolve_discussion_open_issue.png)bin20967 -> 20967 bytes
-rw-r--r--doc/user/discussions/index.md150
-rw-r--r--doc/user/permissions.md3
-rw-r--r--doc/user/profile/account/two_factor_authentication.md2
-rw-r--r--doc/user/project/cycle_analytics.md4
-rw-r--r--doc/user/project/integrations/kubernetes.md10
-rw-r--r--doc/user/project/integrations/microsoft_teams.md6
-rw-r--r--doc/user/project/integrations/project_services.md1
-rw-r--r--doc/user/project/merge_requests/index.md2
-rw-r--r--doc/user/project/merge_requests/merge_request_discussion_resolution.md107
-rw-r--r--doc/user/project/milestones/img/milestone_create.pngbin0 -> 40591 bytes
-rw-r--r--doc/user/project/milestones/img/milestone_group_create.pngbin0 -> 35526 bytes
-rw-r--r--doc/user/project/milestones/index.md46
-rw-r--r--doc/user/search/img/search_history.gifbin0 -> 265970 bytes
-rw-r--r--doc/user/search/index.md6
-rw-r--r--doc/workflow/README.md4
-rw-r--r--doc/workflow/gitlab_flow.md1
-rw-r--r--doc/workflow/groups.md2
-rw-r--r--doc/workflow/importing/import_projects_from_github.md6
-rw-r--r--doc/workflow/milestones.md29
-rw-r--r--doc/workflow/milestones/form.pngbin40414 -> 0 bytes
-rw-r--r--doc/workflow/milestones/group_form.pngbin35820 -> 0 bytes
-rw-r--r--doc/workflow/shortcuts.md6
-rw-r--r--doc/workflow/todos.md2
-rw-r--r--features/group/members.feature34
-rw-r--r--features/project/issues/issues.feature6
-rw-r--r--features/project/merge_requests/revert.feature2
-rw-r--r--features/project/source/browse_files.feature6
-rw-r--r--features/project/team_management.feature20
-rw-r--r--features/steps/group/members.rb69
-rw-r--r--features/steps/project/commits/commits.rb2
-rw-r--r--features/steps/project/issues/award_emoji.rb6
-rw-r--r--features/steps/project/issues/issues.rb11
-rw-r--r--features/steps/project/project.rb6
-rw-r--r--features/steps/project/source/browse_files.rb13
-rw-r--r--features/steps/project/team_management.rb77
-rw-r--r--lib/api/commits.rb4
-rw-r--r--lib/api/entities.rb7
-rw-r--r--lib/api/files.rb4
-rw-r--r--lib/api/groups.rb7
-rw-r--r--lib/api/helpers/internal_helpers.rb12
-rw-r--r--lib/api/internal.rb26
-rw-r--r--lib/api/issues.rb8
-rw-r--r--lib/api/merge_requests.rb23
-rw-r--r--lib/api/project_hooks.rb12
-rw-r--r--lib/api/projects.rb41
-rw-r--r--lib/api/settings.rb67
-rw-r--r--lib/api/users.rb39
-rw-r--r--lib/api/v3/commits.rb4
-rw-r--r--lib/api/v3/files.rb4
-rw-r--r--lib/banzai/filter/issuable_state_filter.rb6
-rw-r--r--lib/banzai/filter/plantuml_filter.rb8
-rw-r--r--lib/banzai/reference_parser/base_parser.rb3
-rw-r--r--lib/banzai/reference_parser/merge_request_parser.rb41
-rw-r--r--lib/banzai/renderer.rb41
-rw-r--r--lib/bitbucket/representation/base.rb6
-rw-r--r--lib/ci/ansi2html.rb2
-rw-r--r--lib/container_registry/path.rb14
-rw-r--r--lib/container_registry/tag.rb4
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb4
-rw-r--r--lib/gitlab/checks/change_access.rb5
-rw-r--r--lib/gitlab/checks/force_push.rb12
-rw-r--r--lib/gitlab/ci/trace/stream.rb22
-rw-r--r--lib/gitlab/database.rb8
-rw-r--r--lib/gitlab/database/migration_helpers.rb250
-rw-r--r--lib/gitlab/database/multi_threaded_migration.rb52
-rw-r--r--lib/gitlab/diff/diff_refs.rb6
-rw-r--r--lib/gitlab/email/handler/create_note_handler.rb6
-rw-r--r--lib/gitlab/email/handler/unsubscribe_handler.rb2
-rw-r--r--lib/gitlab/email/receiver.rb4
-rw-r--r--lib/gitlab/etag_caching/middleware.rb36
-rw-r--r--lib/gitlab/etag_caching/router.rb39
-rw-r--r--lib/gitlab/git/blob.rb4
-rw-r--r--lib/gitlab/git/encoding_helper.rb8
-rw-r--r--lib/gitlab/git/env.rb38
-rw-r--r--lib/gitlab/git/index.rb49
-rw-r--r--lib/gitlab/git/repository.rb80
-rw-r--r--lib/gitlab/git/rev_list.rb49
-rw-r--r--lib/gitlab/git_access.rb4
-rw-r--r--lib/gitlab/gitaly_client.rb2
-rw-r--r--lib/gitlab/markup_helper.rb25
-rw-r--r--lib/gitlab/metrics.rb10
-rw-r--r--lib/gitlab/o_auth/user.rb2
-rw-r--r--lib/gitlab/regex.rb16
-rw-r--r--lib/gitlab/usage_data.rb65
-rw-r--r--lib/gitlab/user_activities.rb34
-rw-r--r--lib/tasks/cache.rake7
-rw-r--r--lib/tasks/gitlab/check.rake14
-rw-r--r--lib/tasks/gitlab/gitaly.rake4
-rw-r--r--lib/tasks/gitlab/shell.rake7
-rw-r--r--lib/tasks/gitlab/task_helpers.rb41
-rw-r--r--lib/tasks/gitlab/update_templates.rake2
-rw-r--r--lib/tasks/gitlab/workhorse.rake4
-rw-r--r--lib/tasks/import.rake1
-rw-r--r--package.json17
-rw-r--r--qa/qa/page/main/menu.rb2
-rwxr-xr-xscripts/prepare_build.sh23
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb37
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb24
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb4
-rw-r--r--spec/controllers/projects/builds_controller_spec.rb2
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb2
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb4
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb10
-rw-r--r--spec/controllers/sessions_controller_spec.rb10
-rw-r--r--spec/factories/merge_requests.rb4
-rw-r--r--spec/factories/notes.rb3
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb48
-rw-r--r--spec/features/discussion_comments/commit_spec.rb18
-rw-r--r--spec/features/discussion_comments/issue_spec.rb16
-rw-r--r--spec/features/discussion_comments/merge_request_spec.rb16
-rw-r--r--spec/features/discussion_comments/snippets_spec.rb16
-rw-r--r--spec/features/groups/members/list_spec.rb54
-rw-r--r--spec/features/groups/milestone_spec.rb36
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb14
-rw-r--r--spec/features/issues/note_polling_spec.rb14
-rw-r--r--spec/features/issues_spec.rb7
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb8
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_spec.rb238
-rw-r--r--spec/features/merge_requests/discussion_spec.rb51
-rw-r--r--spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb13
-rw-r--r--spec/features/merge_requests/user_posts_diff_notes_spec.rb294
-rw-r--r--spec/features/merge_requests/user_posts_notes.rb145
-rw-r--r--spec/features/merge_requests/user_sees_system_notes_spec.rb31
-rw-r--r--spec/features/merge_requests/versions_spec.rb (renamed from spec/features/merge_requests/merge_request_versions_spec.rb)28
-rw-r--r--spec/features/merge_requests/widget_spec.rb21
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb285
-rw-r--r--spec/features/projects/blobs/user_create_spec.rb4
-rw-r--r--spec/features/projects/files/creating_a_file_spec.rb6
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb2
-rw-r--r--spec/features/projects/issuable_templates_spec.rb10
-rw-r--r--spec/features/projects/members/list_spec.rb90
-rw-r--r--spec/features/projects/view_on_env_spec.rb4
-rw-r--r--spec/features/projects/wiki/shortcuts_spec.rb20
-rw-r--r--spec/fixtures/trace/ansi-sequence-and-unicode5
-rw-r--r--spec/helpers/preferences_helper_spec.rb4
-rw-r--r--spec/helpers/projects_helper_spec.rb23
-rw-r--r--spec/helpers/submodule_helper_spec.rb12
-rw-r--r--spec/javascripts/awards_handler_spec.js53
-rw-r--r--spec/javascripts/blob/blob_fork_suggestion_spec.js37
-rw-r--r--spec/javascripts/blob/sketch/index_spec.js2
-rw-r--r--spec/javascripts/boards/list_spec.js40
-rw-r--r--spec/javascripts/build_spec.js137
-rw-r--r--spec/javascripts/ci_status_icon_spec.js44
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js7
-rw-r--r--spec/javascripts/diff_comments_store_spec.js194
-rw-r--r--spec/javascripts/droplab/constants_spec.js6
-rw-r--r--spec/javascripts/droplab/drop_down_spec.js28
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js2
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js2
-rw-r--r--spec/javascripts/environments/environment_item_spec.js2
-rw-r--r--spec/javascripts/environments/environment_monitoring_spec.js2
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js2
-rw-r--r--spec/javascripts/environments/environment_spec.js5
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js2
-rw-r--r--spec/javascripts/environments/environment_table_spec.js2
-rw-r--r--spec/javascripts/environments/environment_terminal_button_spec.js2
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js5
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js94
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js446
-rw-r--r--spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js144
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js394
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js208
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js256
-rw-r--r--spec/javascripts/filtered_search/services/recent_searches_service_spec.js2
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb7
-rw-r--r--spec/javascripts/issue_show/issue_title_spec.js2
-rw-r--r--spec/javascripts/issue_spec.js194
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js15
-rw-r--r--spec/javascripts/lib/utils/number_utility_spec.js9
-rw-r--r--spec/javascripts/lib/utils/poll_spec.js93
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js148
-rw-r--r--spec/javascripts/merged_buttons_spec.js44
-rw-r--r--spec/javascripts/notes_spec.js152
-rw-r--r--spec/javascripts/pipelines/async_button_spec.js (renamed from spec/javascripts/vue_pipelines_index/async_button_spec.js)2
-rw-r--r--spec/javascripts/pipelines/empty_state_spec.js (renamed from spec/javascripts/vue_pipelines_index/empty_state_spec.js)2
-rw-r--r--spec/javascripts/pipelines/error_state_spec.js (renamed from spec/javascripts/vue_pipelines_index/error_state_spec.js)2
-rw-r--r--spec/javascripts/pipelines/mock_data.js (renamed from spec/javascripts/vue_pipelines_index/mock_data.js)0
-rw-r--r--spec/javascripts/pipelines/nav_controls_spec.js (renamed from spec/javascripts/vue_pipelines_index/nav_controls_spec.js)2
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js (renamed from spec/javascripts/vue_pipelines_index/pipeline_url_spec.js)2
-rw-r--r--spec/javascripts/pipelines/pipelines_actions_spec.js (renamed from spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js)2
-rw-r--r--spec/javascripts/pipelines/pipelines_artifacts_spec.js (renamed from spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js)2
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js (renamed from spec/javascripts/vue_pipelines_index/pipelines_spec.js)4
-rw-r--r--spec/javascripts/pipelines/pipelines_store_spec.js (renamed from spec/javascripts/vue_pipelines_index/pipelines_store_spec.js)2
-rw-r--r--spec/javascripts/pipelines/stage_spec.js66
-rw-r--r--spec/javascripts/shortcuts_spec.js45
-rw-r--r--spec/javascripts/user_callout_spec.js1
-rw-r--r--spec/lib/banzai/filter/issuable_state_filter_spec.rb157
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb8
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb4
-rw-r--r--spec/lib/banzai/redactor_spec.rb25
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb23
-rw-r--r--spec/lib/banzai/renderer_spec.rb69
-rw-r--r--spec/lib/container_registry/path_spec.rb54
-rw-r--r--spec/lib/container_registry/tag_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb71
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb20
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb432
-rw-r--r--spec/lib/gitlab/database/multi_threaded_migration_spec.rb41
-rw-r--r--spec/lib/gitlab/database_spec.rb8
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb8
-rw-r--r--spec/lib/gitlab/etag_caching/router_spec.rb83
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb4
-rw-r--r--spec/lib/gitlab/git/encoding_helper_spec.rb4
-rw-r--r--spec/lib/gitlab/git/env_spec.rb102
-rw-r--r--spec/lib/gitlab/git/index_spec.rb20
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb107
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb92
-rw-r--r--spec/lib/gitlab/git_access_spec.rb2
-rw-r--r--spec/lib/gitlab/regex_spec.rb6
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb70
-rw-r--r--spec/lib/gitlab/user_activities_spec.rb127
-rw-r--r--spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb49
-rw-r--r--spec/migrations/migrate_user_project_view_spec.rb17
-rw-r--r--spec/migrations/schema_spec.rb23
-rw-r--r--spec/models/abuse_report_spec.rb3
-rw-r--r--spec/models/blob_spec.rb10
-rw-r--r--spec/models/commit_spec.rb55
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb270
-rw-r--r--spec/models/concerns/ignorable_column_spec.rb38
-rw-r--r--spec/models/container_repository_spec.rb67
-rw-r--r--spec/models/diff_note_spec.rb17
-rw-r--r--spec/models/environment_spec.rb23
-rw-r--r--spec/models/issue_spec.rb14
-rw-r--r--spec/models/label_spec.rb16
-rw-r--r--spec/models/member_spec.rb25
-rw-r--r--spec/models/members/group_member_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb10
-rw-r--r--spec/models/note_spec.rb12
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb20
-rw-r--r--spec/models/repository_spec.rb49
-rw-r--r--spec/models/spam_log_spec.rb11
-rw-r--r--spec/models/user_spec.rb14
-rw-r--r--spec/policies/group_policy_spec.rb3
-rw-r--r--spec/requests/api/commits_spec.rb3
-rw-r--r--spec/requests/api/files_spec.rb6
-rw-r--r--spec/requests/api/internal_spec.rb36
-rw-r--r--spec/requests/api/project_hooks_spec.rb13
-rw-r--r--spec/requests/api/projects_spec.rb42
-rw-r--r--spec/requests/api/users_spec.rb146
-rw-r--r--spec/requests/api/v3/commits_spec.rb3
-rw-r--r--spec/requests/api/v3/files_spec.rb6
-rw-r--r--spec/requests/git_http_spec.rb9
-rw-r--r--spec/routing/project_routing_spec.rb2
-rw-r--r--spec/serializers/build_serializer_spec.rb2
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb2
-rw-r--r--spec/services/cohorts_service_spec.rb99
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb13
-rw-r--r--spec/services/event_create_service_spec.rb15
-rw-r--r--spec/services/files/update_service_spec.rb6
-rw-r--r--spec/services/groups/destroy_service_spec.rb2
-rw-r--r--spec/services/members/authorized_destroy_service_spec.rb45
-rw-r--r--spec/services/notification_service_spec.rb16
-rw-r--r--spec/services/projects/create_service_spec.rb14
-rw-r--r--spec/services/projects/import_service_spec.rb9
-rw-r--r--spec/services/search/global_service_spec.rb21
-rw-r--r--spec/services/search/group_service_spec.rb40
-rw-r--r--spec/services/users/activity_service_spec.rb48
-rw-r--r--spec/services/users/build_service_spec.rb55
-rw-r--r--spec/services/users/create_service_spec.rb78
-rw-r--r--spec/services/users/destroy_service_spec.rb6
-rw-r--r--spec/services/users/migrate_to_ghost_user_service_spec.rb18
-rw-r--r--spec/spec_helper.rb14
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb (renamed from spec/features/discussion_comments_spec.rb)114
-rw-r--r--spec/support/fixture_helpers.rb7
-rw-r--r--spec/support/gitaly.rb7
-rw-r--r--spec/support/matchers/user_activity_matchers.rb5
-rw-r--r--spec/support/mobile_helpers.rb4
-rw-r--r--spec/support/services/migrate_to_ghost_user_service_shared_examples.rb52
-rw-r--r--spec/support/test_env.rb30
-rw-r--r--spec/support/user_activities_helpers.rb7
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb12
-rw-r--r--spec/tasks/gitlab/task_helpers_spec.rb73
-rw-r--r--spec/tasks/gitlab/workhorse_rake_spec.rb12
-rw-r--r--spec/views/layouts/nav/_project.html.haml_spec.rb37
-rw-r--r--spec/views/projects/registry/repositories/index.html.haml_spec.rb36
-rw-r--r--spec/workers/gitlab_usage_ping_worker_spec.rb23
-rw-r--r--spec/workers/schedule_update_user_activity_worker_spec.rb25
-rw-r--r--spec/workers/update_user_activity_worker_spec.rb35
-rw-r--r--vendor/assets/javascripts/notebooklab.js304
-rw-r--r--vendor/assets/javascripts/pdf.worker.js56
-rw-r--r--vendor/assets/javascripts/pdflab.js271
-rw-r--r--vendor/gitignore/C.gitignore1
-rw-r--r--vendor/gitignore/Dart.gitignore27
-rw-r--r--vendor/gitignore/Global/Eclipse.gitignore6
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore3
-rw-r--r--vendor/gitignore/Global/macOS.gitignore51
-rw-r--r--vendor/gitignore/Python.gitignore3
-rw-r--r--vendor/gitignore/Rails.gitignore2
-rw-r--r--vendor/gitignore/TeX.gitignore3
-rw-r--r--vendor/gitignore/Unity.gitignore1
-rw-r--r--vendor/gitignore/VisualStudio.gitignore3
-rw-r--r--vendor/gitlab-ci-yml/CONTRIBUTING.md5
-rw-r--r--vendor/gitlab-ci-yml/Django.gitlab-ci.yml17
-rw-r--r--vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml4
-rw-r--r--vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml13
-rw-r--r--vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml6
-rw-r--r--vendor/gitlab-ci-yml/Scala.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml84
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml6
-rw-r--r--vendor/licenses.csv468
-rw-r--r--yarn.lock1206
1039 files changed, 18475 insertions, 10815 deletions
diff --git a/.eslintrc b/.eslintrc
index b0ae2a31919..aba8112c5a9 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -13,9 +13,12 @@
},
"plugins": [
"filenames",
- "import"
+ "import",
+ "html",
+ "promise"
],
"settings": {
+ "html/html-extensions": [".html", ".html.raw", ".vue"],
"import/resolver": {
"webpack": {
"config": "./config/webpack.config.js"
@@ -24,6 +27,7 @@
},
"rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+$"],
- "no-multiple-empty-lines": ["error", { "max": 1 }]
+ "no-multiple-empty-lines": ["error", { "max": 1 }],
+ "promise/catch-or-return": "error"
}
}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2d3e3dcd976..f188ee29223 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-git-2.7-phantomjs-2.1-node-7.1"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-phantomjs-2.1-node-7.1"
cache:
key: "ruby-233"
@@ -67,6 +67,7 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
+ - export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack rspec "--color --format documentation"
artifacts:
@@ -87,6 +88,7 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
+ - export CACHE_CLASSES=true
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
@@ -199,7 +201,13 @@ rake config_lint: *exec
rake brakeman: *exec
rake flay: *exec
license_finder: *exec
-rake downtime_check: *exec
+rake downtime_check:
+ <<: *exec
+ except:
+ - master
+ - tags
+ - /^[\d-]+-stable(-ee)?$/
+
rake ee_compat_check:
<<: *exec
only:
@@ -276,7 +284,6 @@ rake karma:
cache:
paths:
- vendor/ruby
- - node_modules
stage: test
<<: *use-db
<<: *dedicated-runner
@@ -349,8 +356,6 @@ migration paths:
script:
- git fetch origin v8.14.10
- git checkout -f FETCH_HEAD
- - cp config/resque.yml.example config/resque.yml
- - sed -i 's/localhost/redis/g' config/resque.yml
- bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_COMMIT_SHA
@@ -377,9 +382,6 @@ coverage:
lint:javascript:
<<: *dedicated-runner
- cache:
- paths:
- - node_modules/
stage: test
before_script: []
script:
@@ -387,9 +389,6 @@ lint:javascript:
lint:javascript:report:
<<: *dedicated-runner
- cache:
- paths:
- - node_modules/
stage: post-test
before_script: []
script:
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index 34c2e097ba8..6bb21e6a3af 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -25,14 +25,20 @@ logs, and code as it's very hard to read otherwise.)
#### Results of GitLab environment info
+<details>
+
(For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:env:info`)
(For installations from source run and paste the output of:
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
+</details>
+
#### Results of GitLab application Check
+<details>
+
(For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:check SANITIZE=true`)
@@ -41,6 +47,8 @@ logs, and code as it's very hard to read otherwise.)
(we will only investigate if the tests are passing)
+</details>
+
### Possible fixes
(If you can, link to the line of code that might be responsible for the problem)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 712a4970a41..977a7927615 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,272 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 9.1.0 (2017-04-22)
+
+- Added merge requests empty state. !7342
+- Add option to start a new resolvable discussion in an MR. !7527
+- Hide form inputs for group member without editing rights. !7816
+- Create a new issue for a single discussion in a Merge Request. !8266 (Bob Van Landuyt)
+- Adding non_archived scope for counting projects. !8305 (Naveen Kumar)
+- Don't show links to tag a commit for users that are not permitted. !8407
+- New file from interface on existing branch. !8427 (Jacopo Beschi @jacopo-beschi)
+- Strip reference prefixes on branch creation. !8498 (Matthieu Tardy)
+- Support 2FA requirement per-group. !8763 (Markus Koller)
+- Add Undo to Todos in the Done tab. !8782 (Jacopo Beschi @jacopo-beschi)
+- Shows 'Go Back' link only when browser history is available. !9017
+- Implement user create service. !9220 (George Andrinopoulos)
+- Incorporate Gitaly client for refs service. !9291
+- Cancel pending pipelines if commits not HEAD. !9362 (Rydkin Maxim)
+- Add indication for closed or merged issuables in GFM. !9462 (Adam Buckland)
+- Periodically clean up temporary upload files to recover storage space. !9466 (blackst0ne)
+- Use toggle button to expand / collapse mulit-nested groups. !9501
+- Fixes dismissable error close is not visible enough. !9516
+- Fixes an issue in the new merge request form, where a tag would be selected instead of a branch when they have the same names. !9535 (Weiqing Chu)
+- Expose CI/CD status API endpoints with Gitlab::Ci::Status facility on pipeline, job and merge request for favicon. !9561 (dosuken123)
+- Use Gitaly for CommitController#show. !9629
+- Order milestone issues by position ascending in api. !9635 (George Andrinopoulos)
+- Convert Issue into ES6 class. !9636 (winniehell)
+- Link issuable reference to itself in meta-header. !9641 (mhasbini)
+- Add ability to disable Merge Request URL on push. !9663 (Alex Sanford)
+- ProjectsFinder should handle more options. !9682 (Jacopo Beschi @jacopo-beschi)
+- Fix create issue form buttons are misaligned on mobile. !9706 (TM Lee)
+- Labels support color names in backend. !9725 (Dongqing Hu)
+- Standardize on core-js for es2015 polyfills. !9749
+- Fix GitHub Import deleting branches for open PRs from a fork. !9758
+- Do not show LFS object when LFS is disabled. !9779 (Christopher Bartz)
+- Fix symlink icon in project tree. !9780 (mhasbini)
+- Fix bug when system hook for deploy key. !9796 (billy.lb)
+- Make authorized projects worker use a specific queue instead of the default one. !9813
+- Simplify trigger_docs build job for CE and EE. !9820 (winniehell)
+- Add `aria-label` for feature status accessibility. !9830
+- Add dashboard and group milestones count badges. !9836 (Alex Braha Stoll)
+- Use Gitaly for Repository#is_ancestor. !9864
+- After copying a diff file or blob path, pasting it into a comment field will format it as Markdown. !9876
+- Fix visibility level on new project page. !9885 (blackst0ne)
+- Fix xml.updated field in rss/atom feeds. !9889 (blackst0ne)
+- Add Undo mark all as done to Todos. !9890 (Jacopo Beschi @jacopo-beschi)
+- Add a name field to the group form. !9891 (Douglas Lovell)
+- Add custom attributes in factories. !9892 (George Andrinopoulos)
+- Resolve project pipeline status caching problem on dashboard. !9895
+- Display error message when deleting tag in web UI fails. !9906
+- Add quick submit for snippet forms. !9911 (blackst0ne)
+- New directory from interface on existing branch. !9921 (Jacopo Beschi @jacopo-beschi)
+- Removes UJS from pipelines tables. !9929
+- Fix project title validation, prevent clicking on disabled button. !9931
+- Show correct user & creation time in heading of the pipeline page. !9936
+- Include time tracking attributes in webhooks payload. !9942
+- Add `requirements: { id: /.+/ }` for all projects and groups namespaced API routes. !9944
+- Improved UX for the environments metrics view. !9946
+- Remove whitespace in group links. !9947 (Xurxo Méndez Pérez)
+- Adds Frontend Styleguide to documentation. !9961
+- Add metadata to system notes. !9964
+- When viewing old wiki page version, edit button should be disabled. !9966 (TM Lee)
+- Added labels array to the issue web hook returned object. !9972
+- Upgrade VueJS to v2.2.4 and disable dev mode warnings. !9981
+- Only add code coverage instrumentation when generating coverage report. !9987
+- Fix Project Wiki update. !9990 (Dongqing Hu)
+- Fix trigger webhook for ref with a dot. !10001 (George Andrinopoulos)
+- Fix quick submit short-cut on preview tab for comments. !10002
+- Add option to receive email notifications about your own activity. !10032 (Richard Macklin)
+- Rename 'All issues' to 'Open issues' in Add issues modal. !10042 (blackst0ne)
+- Disable pipeline and environment actions that are not playable. !10052
+- Added clarification to the Jira integration documentation. !10066 (Matthew Bender)
+- Move milestone summary content into the sidebar. !10096
+- Replace closing MR icon. !10103 (blackst0ne)
+- Add support for multi-level container image repository names. !10109 (André Guede)
+- Add ECMAScript polyfills for Symbol and Array.find. !10120
+- Add tooltip to user's calendar activities. !10123 (Alex Argunov)
+- Resolve "Run CI/CD pipelines on a schedule" - "Basic backend implementation". !10133 (dosuken123)
+- Change hint on first row of filters dropdown to `Press Enter or click to search`. !10138
+- Remove useless queries with false conditions (e.g 1=0). !10141 (mhasbini)
+- Show CI status as Favicon on Pipelines, Job and MR pages. !10144
+- Update color palette to a more harmonious and consistent one. !10154
+- Add tooltip and accessibility for profile cover buttons. !10182
+- Change Done column to Closed in issue boards. !10198 (blackst0ne)
+- Add metrics button to environments overview page. !10234
+- Force unlimited terminal size when checking processes via call to ps. !10246 (Sebastian Reitenbach)
+- Fix sub-nav highlighting for `Environments` and `Jobs` pages. !10254
+- Drop support for correctly processing legacy pipelines. !10266
+- Fix project creation failure due to race condition in namespace directory creation. !10268 (Robin Bobbitt)
+- Introduced error/empty states for the environments performance metrics. !10271
+- Improve performance of GitHub importer for large repositories. !10273
+- Introduce "polling_interval_multiplier" as application setting. !10280
+- Prevent users from disconnecting GitLab account from CAS. !10282
+- Clearly show who triggered the pipeline in email. !10283
+- Make user mentions case-insensitive. !10285 (blackst0ne)
+- Update rugged to 0.25.1.1. !10286 (Elan Ruusamäe)
+- Handle parsing OpenBSD ps output properly to display sidekiq infos on admin->monitoring->background. !10303 (Sebastian Reitenbach)
+- Log errors during generating of Gitlab Pages to debug log. !10335 (Danilo Bargen)
+- Update issue board cards design. !10353
+- Tags can be protected, restricting creation of matching tags by user role. !10356
+- Set GIT_TERMINAL_PROMPT env variable in initializer. !10372
+- Remove index for users.current sign in at. !10401 (blackst0ne)
+- Include reopened MRs when searching for opened ones. !10407
+- Integrates Microsoft Teams webhooks with GitLab. !10412
+- Fix subgroup repository disappearance if group was moved. !10414
+- Add /-/readiness /-/liveness and /-/metrics endpoints to track application health. !10416
+- Changed capitalisation of buttons across GitLab. !10418
+- Fix blob highlighting in search. !10420
+- Add remove_concurrent_index to database helper. !10441 (blackst0ne)
+- Fix wiki commit message. !10464 (blackst0ne)
+- Deleting a user should not delete associated records. !10467
+- Include endpoint in metrics for ETag caching middleware. !10495
+- Change project view default for existing users and anonymous visitors to files+readme. !10498
+- Hide header counters for issue/mr/todos if zero. !10506
+- Remove the User#is_admin? method. !10520 (blackst0ne)
+- Removed Milestone#is_empty?. !10523 (Jacopo Beschi @jacopo-beschi)
+- Add UI for Trigger Schedule. !10533 (dosuken123)
+- Add foreign key for ci_trigger_requests on ci_triggers. !10537
+- Upgrade webpack to v2.3.3 and webpack-dev-server to v2.4.2. !10552
+- Bugfix: POST /projects/:id/hooks and PUT /projects/:id/hook/:hook_id no longer ignore the the job_events param in the V4 API. !10586
+- Fix MR widget bug that merged a MR when Merge when pipeline succeeds was clicked via the dropdown. !10611
+- Hide new subgroup button if user has no permission to create one. !10627
+- Fix PlantUML integration in GFM. !10651
+- Show sub-nav under Merge Requests when issue tracker is non-default. !10658
+- Fix bad query for PostgreSQL showing merge requests list. !10666
+- Fix invalid encoding when showing some traces. !10681
+- Add lighter colors and fix existing light colors. !10690
+- Fix another case where trace does not have proper encoding set. !10728
+- Fix trace cannot be written due to encoding. !10758
+- Replace builds_enabled with jobs_enabled in projects API v4. !10786 (winniehell)
+- Add retry to system hook worker. !10801
+- Fix error when an issue reference has a pending deleting project. !10843
+- Update permalink/blame buttons with line number fragment hash.
+- Limit line length for project home page.
+- Fix filtered search input width for IE.
+- Update wikis_controller.rb to use strong params.
+- Fix API group/issues default state filter. (Alexander Randa)
+- Prevent builds dropdown to close when the user clicks in a build.
+- Display all closed issues in “done” board list.
+- Remove no-new annotation from file_template_mediator.js.
+- Changed dropdown style slightly.
+- Change gfm textarea to use monospace font.
+- Prevent filtering issues by multiple Milestones or Authors.
+- Recent search history for issues.
+- Remove duplicated tokens in issuable search bar.
+- Adds empty and error state to pipelines.
+- Allow admin to view all namespaces. (George Andrinopoulos)
+- allow offset query parameter for infinite list pages.
+- Fix wrong message on starred projects filtering. (George Andrinopoulos)
+- Adds pipeline mini-graph to system information box in Commit View.
+- Remove confusing placeholder for JIRA transition_id.
+- Remove extra margin at bottom of todos page.
+- Add back expandable folder behavior.
+- Create todos only for new mentions.
+- Linking to blob edit page handles anonymous users and users without enough permissions to edit directly.
+- Fix projects_limit RangeError on user create. (Alexander Randa)
+- Add helpful icons to profile events.
+- Refactor dropdown_milestone_spec.rb. (George Andrinopoulos)
+- Fix alignment of resolve button.
+- Change label for name on sign up form.
+- Don’t show source project name when user does not have access.
+- Update toggle buttons to be <button>.
+- Display full project name with namespace upon deletion.
+- Spam check only when spammable attributes have changed.
+- align Mark all as done with other Done buttons on Todos page.
+- Adds polling utility function for vue resource.
+- Allow unauthenticated access to some Branch API GET endpoints.
+- Fix redirection after login when the referer have params. (mhasbini)
+- fix sidebar padding for build and wiki pages.
+- Correctly update paths when changing a child group.
+- Add shortcuts and counters to MRs and issues in navbar.
+- Remove forced scroll into view when switching to Changes MR tab.
+- Fix link to Jira service documentation.
+- consistent icons in vue and kaminari pagers.
+- refocus textarea after attaching a file.
+- Enable creation of deploy keys with write access via the API.
+- Disable invalid service templates.
+- Remove the class attribute from the whitelist for HTML generated from Markdown.
+- Add search optional param and docs for V4.
+- Fix issue's note cache expiration after delete. (mhasbini)
+- Fixes HTML structure that was preventing the tooltip to disappear when hovering out of the button.
+- fix Status icons overlapping sidebar on mobile.
+- Add dropdown sort to project milestones. (George Andrinopoulos)
+- Prevent more than one issue tracker to be active for the same project. (luisdgs19)
+- Add copy button to blob header and use icon for Raw button.
+- Add metrics events for incoming emails.
+- Shows loading icon in issue boards modal when changing filters.
+- Added tests for the w.gl.utils.backOff promise.
+- Add `g t` global shortcut to go to todos.
+- Fix conflict resolution when files contain valid UTF-8 characters.
+- Added award emoji animation and improved active state.
+- Fixes milestone/merge_requests endpoint to actually scope the result. (Joren De Groof)
+- Added remaining_time method to milestoneish, specs and updated the milestone_helper milestone_remaining_days method to correctly return the correct remaining time. (Michael Robinson)
+- Removed unnecessary 'add' text in additional award emoji button.
+- adds todo functionality to closed issuable sidebar and changes todo bell icon to check-square.
+- Copy code as GFM from diffs, blobs and GFM code blocks.
+- Removed the duplicated search icon in the award emoji menu.
+- Enable snippets for new projects by default.
+- Add rake task to import GitHub projects from the command line.
+- New rake task to reset all email and private tokens.
+- Fix path disclosure in project import/export.
+- Fix 'Object not found - no match for id (sha)' when importing GitHub Pull Requests.
+- Display custom hook error messages when automatic merge is enabled.
+- Fix layout of projects page on admin area.
+- Fix encoding issue exporting a project.
+- Periodically mark projects that are stuck in importing as failed.
+- Skip groups validation on the client.
+- Fix Import/Export MR diffs not showing and missing forked MRs.
+- Create subgroups if they don't exist while importing projects.
+- Fix Milestone name on show page. (Raveesh)
+- Fix missing capitalisation on views.
+- Removed orphaned notification settings without a namespace.
+- Fix restricted project visibility setting available to users.
+- Moved the gear settings dropdown to a tab in the groups view.
+- Fixed group milestone date dropdowns not opening.
+- Fixed bug in issue boards which stopped cards being able to be dragged.
+- Added new filtered search bar to issue boards.
+- Add closed_at field to issues.
+- Do not set closed_at to nil when issue is reopened.
+- Centered issues empty state.
+- Fixed private group name disclosure via new/update forms.
+- Add keyboard shortcuts to main menu.
+- Moved the monitoring button inside the show view for the environments page.
+- Speed up initial rendering of MR diffs page.
+- Fixed tabs on new merge request page causing incorrect URLs.
+- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
+- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
+- Optimise builds endpoint.
+- Fixed pipeline actions tooltips overflowing.
+- Fixed job tooltip being cut-off.
+- Fixed projects list lines breaking.
+- Only email pipeline creators; only email for successful pipelines with custom settings.
+- Reset users.authorized_projects_populated to automatically refresh user permissions.
+- Corrected alignment for the remember-me checkbox in the login view.
+- Fixed tabs not scrolling on mobile.
+- Add unique index for notes_id to system note metadata table.
+- Handle SSH keys that have multiple spaces between each marker.
+- Don't delete a branch involved in an open merge request in "Delete all merged branches" service.
+- Relax constraint on Wiki IDs, since subdirectories can contain spaces.
+- Remove Tags filter from Projects Explore dropdown.
+- Enable Style/Proc cop for rubocop. (mhasbini)
+- Show the build/pipeline coverage if it is available.
+- Corrected time tracking icon color in the issuable side bar.
+- update test_bundle.js ignored files.
+- Add usage ping to CE.
+- User callout only shows on current users profile.
+- Removed the hours & minutes from the users start date on their profile.
+- Only send chat notifications for the default branch.
+- Don't fill in the default kubernetes namespace.
+
+## 9.0.6 (2017-04-21)
+
+- Bugfix: POST /projects/:id/hooks and PUT /projects/:id/hook/:hook_id no longer ignore the the job_events param in the V4 API. !10586
+- Fix MR widget bug that merged a MR when Merge when pipeline succeeds was clicked via the dropdown. !10611
+- Fix PlantUML integration in GFM. !10651
+- Show sub-nav under Merge Requests when issue tracker is non-default. !10658
+- Fix restricted project visibility setting available to users.
+- Removed orphaned notification settings without a namespace.
+- Fix issue's note cache expiration after delete. (mhasbini)
+- Display custom hook error messages when automatic merge is enabled.
+- Fix filtered search input width for IE.
+
+## 9.0.5 (2017-04-10)
+
+- Add shortcuts and counters to MRs and issues in navbar.
+- Disable invalid service templates.
+- Handle SSH keys that have multiple spaces between each marker.
+
## 9.0.4 (2017-04-05)
- Don’t show source project name when user does not have access.
diff --git a/Gemfile b/Gemfile
index d4b2ade4243..ad8db206da6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -73,6 +73,9 @@ gem 'grape', '~> 0.19.0'
gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
+# Disable strong_params so that Mash does not respond to :permitted?
+gem 'hashie-forbidden_attributes'
+
# Pagination
gem 'kaminari', '~> 0.17.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index d7e3f7343d0..bb91db1e805 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -346,6 +346,8 @@ GEM
tilt
hashdiff (0.3.2)
hashie (3.5.5)
+ hashie-forbidden_attributes (0.1.1)
+ hashie (>= 3.0)
health_check (2.6.0)
rails (>= 4.0)
hipchat (1.5.2)
@@ -915,6 +917,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0)
haml_lint (~> 0.21.0)
hamlit (~> 2.6.1)
+ hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
@@ -1035,4 +1038,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.14.5
+ 1.14.6
diff --git a/PROCESS.md b/PROCESS.md
index cfa841dc13d..fac3c22e09f 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -57,16 +57,16 @@ star, smile, etc.). Some good tips about code reviews can be found in our
[Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html
-## Feature Freeze
+## Feature freeze on the 7th for the release on the 22nd
-After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
+After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
### Between the 1st and the 7th
-These types of merge requests need special consideration:
+These types of merge requests for the upcoming release need special consideration:
* **Large features**: a large feature is one that is highlighted in the kick-off
and the release blogpost; typically this will have its own channel in Slack
@@ -114,14 +114,15 @@ subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts].
-### Between the 7th and the 22nd
+### After the 7th
Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release)
and security issues will be cherry-picked into the stable branch.
Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
-These fixes will be released in the next RC (before the 22nd) or patch release (after the 22nd).
+These fixes will be shipped in the next RC for that release if it is before the 22nd.
+If the fixes are are completed on or after the 22nd, they will be shipped in a patch for that release.
-If you think a merge request should go into the upcoming release even though it does not meet these requirements,
+If you think a merge request should go into an RC or patch even though it does not meet these requirements,
you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer:
1. a Release Manager
diff --git a/README.md b/README.md
index f0e3b52ef6f..10d69efdc6b 100644
--- a/README.md
+++ b/README.md
@@ -73,7 +73,7 @@ One small thing you also have to do when installing it yourself is to copy the e
cp config/unicorn.rb.example.development config/unicorn.rb
-Instructions on how to start GitLab and how to run the tests can be found in the [development section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#development).
+Instructions on how to start GitLab and how to run the tests can be found in the [getting started section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#getting-started).
## Software stack
diff --git a/VERSION b/VERSION
index c3996a4a61f..5c906509f70 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-9.1.0-pre
+9.2.0-pre
diff --git a/app/assets/images/ci_favicons/icon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico
index 5a19458f2a2..5a19458f2a2 100755
--- a/app/assets/images/ci_favicons/icon_status_canceled.ico
+++ b/app/assets/images/ci_favicons/favicon_status_canceled.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico
index 4dca9640cb3..4dca9640cb3 100755
--- a/app/assets/images/ci_favicons/icon_status_created.ico
+++ b/app/assets/images/ci_favicons/favicon_status_created.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico
index c961ff9a69b..c961ff9a69b 100755
--- a/app/assets/images/ci_favicons/icon_status_failed.ico
+++ b/app/assets/images/ci_favicons/favicon_status_failed.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico
index 5fbbc99ea7c..5fbbc99ea7c 100755
--- a/app/assets/images/ci_favicons/icon_status_manual.ico
+++ b/app/assets/images/ci_favicons/favicon_status_manual.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico
index 21afa9c72e6..21afa9c72e6 100755
--- a/app/assets/images/ci_favicons/icon_status_not_found.ico
+++ b/app/assets/images/ci_favicons/favicon_status_not_found.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico
index 8be32dab85a..8be32dab85a 100755
--- a/app/assets/images/ci_favicons/icon_status_pending.ico
+++ b/app/assets/images/ci_favicons/favicon_status_pending.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico
index f328ff1a5ed..f328ff1a5ed 100755
--- a/app/assets/images/ci_favicons/icon_status_running.ico
+++ b/app/assets/images/ci_favicons/favicon_status_running.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico
index b4394e1b4af..b4394e1b4af 100755
--- a/app/assets/images/ci_favicons/icon_status_skipped.ico
+++ b/app/assets/images/ci_favicons/favicon_status_skipped.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico
index 4f436c95242..4f436c95242 100755
--- a/app/assets/images/ci_favicons/icon_status_success.ico
+++ b/app/assets/images/ci_favicons/favicon_status_success.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico
index 805cc20cdec..805cc20cdec 100755
--- a/app/assets/images/ci_favicons/icon_status_warning.ico
+++ b/app/assets/images/ci_favicons/favicon_status_warning.ico
Binary files differ
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index ce426741637..adb45b0606d 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,3 +1,5 @@
+/* global Flash */
+
import Cookies from 'js-cookie';
import emojiMap from 'emojis/digests.json';
@@ -6,6 +8,7 @@ import { glEmojiTag } from './behaviors/gl_emoji';
import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
+const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
const requestAnimationFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
@@ -103,8 +106,9 @@ function AwardsHandler() {
const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon');
const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
+
$target.closest('.js-awards-block').addClass('current');
- return this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
+ this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
});
}
@@ -124,16 +128,18 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
}
const $menu = $('.emoji-menu');
+ const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
+ const $userAuthored = this.isUserAuthored($addBtn);
if ($menu.length) {
if ($menu.is('.is-visible')) {
$addBtn.removeClass('is-active');
$menu.removeClass('is-visible');
- $('#emoji_search').blur();
+ $('.js-emoji-menu-search').blur();
} else {
$addBtn.addClass('is-active');
this.positionMenu($menu, $addBtn);
$menu.addClass('is-visible');
- $('#emoji_search').focus();
+ $('.js-emoji-menu-search').focus();
}
} else {
$addBtn.addClass('is-loading is-active');
@@ -143,10 +149,12 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
this.positionMenu($createdMenu, $addBtn);
return setTimeout(() => {
$createdMenu.addClass('is-visible');
- $('#emoji_search').focus();
+ $('.js-emoji-menu-search').focus();
}, 200);
});
}
+
+ $thumbsBtn.toggleClass('disabled', $userAuthored);
};
// Create the emoji menu with the first category of emojis.
@@ -174,7 +182,7 @@ AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) {
const emojiMenuMarkup = `
<div class="emoji-menu">
- <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" />
+ <input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
<div class="emoji-menu-content">
${frequentlyUsedCatgegory}
@@ -231,6 +239,9 @@ AwardsHandler
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}`);
});
};
@@ -259,7 +270,8 @@ AwardsHandler.prototype.addAward = function addAward(
callback,
) {
const normalizedEmoji = this.normalizeEmojiName(emoji);
- this.postEmoji(awardUrl, normalizedEmoji, () => {
+ const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
+ this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
return typeof callback === 'function' ? callback() : undefined;
});
@@ -324,6 +336,10 @@ AwardsHandler.prototype.isActive = function isActive($emojiButton) {
return $emojiButton.hasClass('active');
};
+AwardsHandler.prototype.isUserAuthored = function isUserAuthored($button) {
+ return $button.hasClass('js-user-authored');
+};
+
AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) {
const counter = $('.js-counter', $emojiButton);
const counterNumber = parseInt(counter.text(), 10);
@@ -428,20 +444,35 @@ AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) {
});
};
-AwardsHandler.prototype.postEmoji = function postEmoji(awardUrl, emoji, callback) {
- return $.post(awardUrl, {
- name: emoji,
- }, (data) => {
- if (data.ok) {
- callback();
- }
- });
+AwardsHandler.prototype.postEmoji = function postEmoji($emojiButton, awardUrl, emoji, callback) {
+ if (this.isUserAuthored($emojiButton)) {
+ this.userAuthored($emojiButton);
+ } else {
+ $.post(awardUrl, {
+ name: emoji,
+ }, (data) => {
+ if (data.ok) {
+ callback();
+ }
+ }).fail(() => new Flash('Something went wrong on our end.'));
+ }
};
AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) {
return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
};
+AwardsHandler.prototype.userAuthored = function userAuthored($emojiButton) {
+ const oldTitle = this.getAwardTooltip($emojiButton);
+ const newTitle = 'You cannot vote on your own issue, MR and note';
+ gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show');
+ // Restore tooltip back to award list
+ return setTimeout(() => {
+ $emojiButton.tooltip('hide');
+ gl.utils.updateTooltipTitle($emojiButton, oldTitle);
+ }, 2800);
+};
+
AwardsHandler.prototype.scrollToAwards = function scrollToAwards() {
const options = {
scrollTop: $('.awards').offset().top - 110,
@@ -474,24 +505,41 @@ AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmoj
};
AwardsHandler.prototype.setupSearch = function setupSearch() {
- this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => {
+ const $search = $('.js-emoji-menu-search');
+
+ this.registerEventListener('on', $search, 'input', (e) => {
const term = $(e.target).val().trim();
- // Clean previous search results
- $('ul.emoji-menu-search, h5.emoji-search-title').remove();
- if (term.length > 0) {
- // Generate a search result block
- const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
- const foundEmojis = this.searchEmojis(term).show();
- 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);
- } else {
- $('.emoji-menu-content').children().show();
+ this.searchEmojis(term);
+ });
+
+ const $menu = $('.emoji-menu');
+ this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
+ if (e.target === e.currentTarget) {
+ // Clear the search
+ this.searchEmojis('');
}
});
};
AwardsHandler.prototype.searchEmojis = function searchEmojis(term) {
+ const $search = $('.js-emoji-menu-search');
+ $search.val(term);
+
+ // Clean previous search results
+ $('ul.emoji-menu-search, h5.emoji-search-title').remove();
+ if (term.length > 0) {
+ // 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);
+ $('.emoji-menu-content ul, .emoji-menu-content h5').hide();
+ $('.emoji-menu-content').append(h5).append(ul);
+ } else {
+ $('.emoji-menu-content').children().show();
+ }
+};
+
+AwardsHandler.prototype.findMatchingEmojiElements = function findMatchingEmojiElements(term) {
const safeTerm = term.toLowerCase();
const namesMatchingAlias = [];
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 4c9ad128e6c..77e92ff8caf 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -22,6 +22,7 @@ $(() => {
}
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
+ e.target.classList.toggle('open');
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase();
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index c9fe23aec75..4568b86f298 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -35,7 +35,7 @@ export default class BlobFileDropzone {
this.removeFile(file);
});
this.on('sending', function (file, xhr, formData) {
- formData.append('target_branch', form.find('input[name="target_branch"]').val());
+ formData.append('branch_name', form.find('input[name="branch_name"]').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val());
});
diff --git a/app/assets/javascripts/blob/blob_fork_suggestion.js b/app/assets/javascripts/blob/blob_fork_suggestion.js
index aa9a4e1c99a..3baf81905fe 100644
--- a/app/assets/javascripts/blob/blob_fork_suggestion.js
+++ b/app/assets/javascripts/blob/blob_fork_suggestion.js
@@ -1,15 +1,63 @@
-function BlobForkSuggestion(openButton, cancelButton, suggestionSection) {
- if (openButton) {
- openButton.addEventListener('click', () => {
+const defaults = {
+ // Buttons that will show the `suggestionSections`
+ // has `data-fork-path`, and `data-action`
+ openButtons: [],
+ // Update the href(from `openButton` -> `data-fork-path`)
+ // whenever a `openButton` is clicked
+ forkButtons: [],
+ // Buttons to hide the `suggestionSections`
+ cancelButtons: [],
+ // Section to show/hide
+ suggestionSections: [],
+ // Pieces of text that need updating depending on the action, `edit`, `replace`, `delete`
+ actionTextPieces: [],
+};
+
+class BlobForkSuggestion {
+ constructor(options) {
+ this.elementMap = Object.assign({}, defaults, options);
+ this.onClickWrapper = this.onClick.bind(this);
+
+ document.addEventListener('click', this.onClickWrapper);
+ }
+
+ showSuggestionSection(forkPath, action = 'edit') {
+ [].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
suggestionSection.classList.remove('hidden');
});
+
+ [].forEach.call(this.elementMap.forkButtons, (forkButton) => {
+ forkButton.setAttribute('href', forkPath);
+ });
+
+ [].forEach.call(this.elementMap.actionTextPieces, (actionTextPiece) => {
+ // eslint-disable-next-line no-param-reassign
+ actionTextPiece.textContent = action;
+ });
}
- if (cancelButton) {
- cancelButton.addEventListener('click', () => {
+ hideSuggestionSection() {
+ [].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
suggestionSection.classList.add('hidden');
});
}
+
+ onClick(e) {
+ const el = e.target;
+
+ if ([].includes.call(this.elementMap.openButtons, el)) {
+ const { forkPath, action } = el.dataset;
+ this.showSuggestionSection(forkPath, action);
+ }
+
+ if ([].includes.call(this.elementMap.cancelButtons, el)) {
+ this.hideSuggestionSection();
+ }
+ }
+
+ destroy() {
+ document.removeEventListener('click', this.onClickWrapper);
+ }
}
export default BlobForkSuggestion;
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index 37094c5c9be..b6dee8177d2 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -1,5 +1,6 @@
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */
+/* global Flash */
import Vue from 'vue';
import VueResource from 'vue-resource';
@@ -85,6 +86,7 @@ $(() => {
if (list.type === 'closed') {
list.position = Infinity;
+ list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
}
});
@@ -92,7 +94,7 @@ $(() => {
Store.addBlankState();
this.loading = false;
- });
+ }).catch(() => new Flash('An error occurred. Please try again.'));
},
methods: {
updateTokens() {
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 93b8960da2e..239eeacf2d7 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -7,100 +7,98 @@ import boardBlankState from './board_blank_state';
require('./board_delete');
require('./board_list');
-(() => {
- const Store = gl.issueBoards.BoardsStore;
+const Store = gl.issueBoards.BoardsStore;
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
- gl.issueBoards.Board = Vue.extend({
- template: '#js-board-template',
- components: {
- boardList,
- 'board-delete': gl.issueBoards.BoardDelete,
- boardBlankState,
- },
- props: {
- list: Object,
- disabled: Boolean,
- issueLinkBase: String,
- rootPath: String,
- },
- data () {
- return {
- detailIssue: Store.detail,
- filter: Store.filter,
- };
- },
- watch: {
- filter: {
- handler() {
- this.list.page = 1;
- this.list.getIssues(true);
- },
- deep: true,
+gl.issueBoards.Board = Vue.extend({
+ template: '#js-board-template',
+ components: {
+ boardList,
+ 'board-delete': gl.issueBoards.BoardDelete,
+ boardBlankState,
+ },
+ props: {
+ list: Object,
+ disabled: Boolean,
+ issueLinkBase: String,
+ rootPath: String,
+ },
+ data () {
+ return {
+ detailIssue: Store.detail,
+ filter: Store.filter,
+ };
+ },
+ watch: {
+ filter: {
+ handler() {
+ this.list.page = 1;
+ this.list.getIssues(true);
},
- detailIssue: {
- handler () {
- if (!Object.keys(this.detailIssue.issue).length) return;
+ deep: true,
+ },
+ detailIssue: {
+ handler () {
+ if (!Object.keys(this.detailIssue.issue).length) return;
- const issue = this.list.findIssue(this.detailIssue.issue.id);
+ const issue = this.list.findIssue(this.detailIssue.issue.id);
- if (issue) {
- const offsetLeft = this.$el.offsetLeft;
- const boardsList = document.querySelectorAll('.boards-list')[0];
- const left = boardsList.scrollLeft - offsetLeft;
- let right = (offsetLeft + this.$el.offsetWidth);
+ if (issue) {
+ const offsetLeft = this.$el.offsetLeft;
+ const boardsList = document.querySelectorAll('.boards-list')[0];
+ const left = boardsList.scrollLeft - offsetLeft;
+ let right = (offsetLeft + this.$el.offsetWidth);
- if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
- // -290 here because width of boardsList is animating so therefore
- // getting the width here is incorrect
- // 290 is the width of the sidebar
- right -= (boardsList.offsetWidth - 290);
- } else {
- right -= boardsList.offsetWidth;
- }
+ if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
+ // -290 here because width of boardsList is animating so therefore
+ // getting the width here is incorrect
+ // 290 is the width of the sidebar
+ right -= (boardsList.offsetWidth - 290);
+ } else {
+ right -= boardsList.offsetWidth;
+ }
- if (right - boardsList.scrollLeft > 0) {
- $(boardsList).animate({
- scrollLeft: right
- }, this.sortableOptions.animation);
- } else if (left > 0) {
- $(boardsList).animate({
- scrollLeft: offsetLeft
- }, this.sortableOptions.animation);
- }
+ if (right - boardsList.scrollLeft > 0) {
+ $(boardsList).animate({
+ scrollLeft: right
+ }, this.sortableOptions.animation);
+ } else if (left > 0) {
+ $(boardsList).animate({
+ scrollLeft: offsetLeft
+ }, this.sortableOptions.animation);
}
- },
- deep: true
- }
- },
- methods: {
- showNewIssueForm() {
- this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
- }
- },
- mounted () {
- this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
- disabled: this.disabled,
- group: 'boards',
- draggable: '.is-draggable',
- handle: '.js-board-handle',
- onEnd: (e) => {
- gl.issueBoards.onEnd();
+ }
+ },
+ deep: true
+ }
+ },
+ methods: {
+ showNewIssueForm() {
+ this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
+ }
+ },
+ mounted () {
+ this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
+ disabled: this.disabled,
+ group: 'boards',
+ draggable: '.is-draggable',
+ handle: '.js-board-handle',
+ onEnd: (e) => {
+ gl.issueBoards.onEnd();
- if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
- const order = this.sortable.toArray();
- const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
+ if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
+ const order = this.sortable.toArray();
+ const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
- this.$nextTick(() => {
- Store.moveList(list, order);
- });
- }
+ this.$nextTick(() => {
+ Store.moveList(list, order);
+ });
}
- });
+ }
+ });
- this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
- },
- });
-})();
+ this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
+ },
+});
diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js
index af621cfd57f..8a1b177bba8 100644
--- a/app/assets/javascripts/boards/components/board_delete.js
+++ b/app/assets/javascripts/boards/components/board_delete.js
@@ -2,22 +2,20 @@
import Vue from 'vue';
-(() => {
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
- gl.issueBoards.BoardDelete = Vue.extend({
- props: {
- list: Object
- },
- methods: {
- deleteBoard () {
- $(this.$el).tooltip('hide');
+gl.issueBoards.BoardDelete = Vue.extend({
+ props: {
+ list: Object
+ },
+ methods: {
+ deleteBoard () {
+ $(this.$el).tooltip('hide');
- if (confirm('Are you sure you want to delete this list?')) {
- this.list.destroy();
- }
+ if (confirm('Are you sure you want to delete this list?')) {
+ this.list.destroy();
}
}
- });
-})();
+ }
+});
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index adbd82cb687..b13386536bf 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -57,12 +57,15 @@ export default {
},
loadNextPage() {
const getIssues = this.list.nextPage();
+ const loadingDone = () => {
+ this.list.loadingMore = false;
+ };
if (getIssues) {
this.list.loadingMore = true;
- getIssues.then(() => {
- this.list.loadingMore = false;
- });
+ getIssues
+ .then(loadingDone)
+ .catch(loadingDone);
}
},
toggleForm() {
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 3c080008244..004bac09f59 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -8,66 +8,64 @@ import Vue from 'vue';
require('./sidebar/remove_issue');
-(() => {
- const Store = gl.issueBoards.BoardsStore;
+const Store = gl.issueBoards.BoardsStore;
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
- gl.issueBoards.BoardSidebar = Vue.extend({
- props: {
- currentUser: Object
- },
- data() {
- return {
- detail: Store.detail,
- issue: {},
- list: {},
- };
- },
- computed: {
- showSidebar () {
- return Object.keys(this.issue).length;
- }
- },
- watch: {
- detail: {
- handler () {
- if (this.issue.id !== this.detail.issue.id) {
- $('.js-issue-board-sidebar', this.$el).each((i, el) => {
- $(el).data('glDropdown').clearMenu();
- });
- }
-
- this.issue = this.detail.issue;
- this.list = this.detail.list;
- },
- deep: true
- },
- issue () {
- if (this.showSidebar) {
- this.$nextTick(() => {
- $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
- $('.right-sidebar').getNiceScroll().resize();
+gl.issueBoards.BoardSidebar = Vue.extend({
+ props: {
+ currentUser: Object
+ },
+ data() {
+ return {
+ detail: Store.detail,
+ issue: {},
+ list: {},
+ };
+ },
+ computed: {
+ showSidebar () {
+ return Object.keys(this.issue).length;
+ }
+ },
+ watch: {
+ detail: {
+ handler () {
+ if (this.issue.id !== this.detail.issue.id) {
+ $('.js-issue-board-sidebar', this.$el).each((i, el) => {
+ $(el).data('glDropdown').clearMenu();
});
}
- }
+
+ this.issue = this.detail.issue;
+ this.list = this.detail.list;
+ },
+ deep: true
},
- methods: {
- closeSidebar () {
- this.detail.issue = {};
+ issue () {
+ if (this.showSidebar) {
+ this.$nextTick(() => {
+ $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
+ $('.right-sidebar').getNiceScroll().resize();
+ });
}
- },
- mounted () {
- new IssuableContext(this.currentUser);
- new MilestoneSelect();
- new gl.DueDateSelectors();
- new LabelsSelect();
- new Sidebar();
- gl.Subscription.bindAll('.subscription');
- },
- components: {
- removeBtn: gl.issueBoards.RemoveIssueBtn,
- },
- });
-})();
+ }
+ },
+ methods: {
+ closeSidebar () {
+ this.detail.issue = {};
+ }
+ },
+ mounted () {
+ new IssuableContext(this.currentUser);
+ new MilestoneSelect();
+ new gl.DueDateSelectors();
+ new LabelsSelect();
+ new Sidebar();
+ gl.Subscription.bindAll('.subscription');
+ },
+ components: {
+ removeBtn: gl.issueBoards.RemoveIssueBtn,
+ },
+});
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index e48d3344a2b..fc154ee7b8b 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -1,141 +1,139 @@
import Vue from 'vue';
import eventHub from '../eventhub';
-(() => {
- const Store = gl.issueBoards.BoardsStore;
+const Store = gl.issueBoards.BoardsStore;
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
- gl.issueBoards.IssueCardInner = Vue.extend({
- props: {
- issue: {
- type: Object,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- list: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- rootPath: {
- type: String,
- required: true,
- },
- updateFilters: {
- type: Boolean,
- required: false,
- default: false,
- },
+gl.issueBoards.IssueCardInner = Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
},
- computed: {
- cardUrl() {
- return `${this.issueLinkBase}/${this.issue.id}`;
- },
- assigneeUrl() {
- return `${this.rootPath}${this.issue.assignee.username}`;
- },
- assigneeUrlTitle() {
- return `Assigned to ${this.issue.assignee.name}`;
- },
- avatarUrlTitle() {
- return `Avatar for ${this.issue.assignee.name}`;
- },
- issueId() {
- return `#${this.issue.id}`;
- },
- showLabelFooter() {
- return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
- },
+ issueLinkBase: {
+ type: String,
+ required: true,
},
- methods: {
- showLabel(label) {
- if (!this.list) return true;
+ list: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ updateFilters: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ cardUrl() {
+ return `${this.issueLinkBase}/${this.issue.id}`;
+ },
+ assigneeUrl() {
+ return `${this.rootPath}${this.issue.assignee.username}`;
+ },
+ assigneeUrlTitle() {
+ return `Assigned to ${this.issue.assignee.name}`;
+ },
+ avatarUrlTitle() {
+ return `Avatar for ${this.issue.assignee.name}`;
+ },
+ issueId() {
+ return `#${this.issue.id}`;
+ },
+ showLabelFooter() {
+ return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
+ },
+ },
+ methods: {
+ showLabel(label) {
+ if (!this.list) return true;
- return !this.list.label || label.id !== this.list.label.id;
- },
- filterByLabel(label, e) {
- if (!this.updateFilters) return;
+ return !this.list.label || label.id !== this.list.label.id;
+ },
+ filterByLabel(label, e) {
+ if (!this.updateFilters) return;
- const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
- const labelTitle = encodeURIComponent(label.title);
- const param = `label_name[]=${labelTitle}`;
- const labelIndex = filterPath.indexOf(param);
- $(e.currentTarget).tooltip('hide');
+ const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
+ const labelTitle = encodeURIComponent(label.title);
+ const param = `label_name[]=${labelTitle}`;
+ const labelIndex = filterPath.indexOf(param);
+ $(e.currentTarget).tooltip('hide');
- if (labelIndex === -1) {
- filterPath.push(param);
- } else {
- filterPath.splice(labelIndex, 1);
- }
+ if (labelIndex === -1) {
+ filterPath.push(param);
+ } else {
+ filterPath.splice(labelIndex, 1);
+ }
- gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
+ gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
- Store.updateFiltersUrl();
+ Store.updateFiltersUrl();
- eventHub.$emit('updateTokens');
- },
- labelStyle(label) {
- return {
- backgroundColor: label.color,
- color: label.textColor,
- };
- },
+ eventHub.$emit('updateTokens');
+ },
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.textColor,
+ };
},
- template: `
- <div>
- <div class="card-header">
- <h4 class="card-title">
- <i
- class="fa fa-eye-slash confidential-icon"
- v-if="issue.confidential"
- aria-hidden="true"
- />
- <a
- class="js-no-trigger"
- :href="cardUrl"
- :title="issue.title">{{ issue.title }}</a>
- <span
- class="card-number"
- v-if="issue.id"
- >
- {{ issueId }}
- </span>
- </h4>
+ },
+ template: `
+ <div>
+ <div class="card-header">
+ <h4 class="card-title">
+ <i
+ class="fa fa-eye-slash confidential-icon"
+ v-if="issue.confidential"
+ aria-hidden="true"
+ />
<a
- class="card-assignee has-tooltip js-no-trigger"
- :href="assigneeUrl"
- :title="assigneeUrlTitle"
- v-if="issue.assignee"
- data-container="body"
+ class="js-no-trigger"
+ :href="cardUrl"
+ :title="issue.title">{{ issue.title }}</a>
+ <span
+ class="card-number"
+ v-if="issue.id"
>
- <img
- class="avatar avatar-inline s20 js-no-trigger"
- :src="issue.assignee.avatar"
- width="20"
- height="20"
- :alt="avatarUrlTitle"
- />
- </a>
- </div>
- <div class="card-footer" v-if="showLabelFooter">
- <button
- class="label color-label has-tooltip js-no-trigger"
- v-for="label in issue.labels"
- type="button"
- v-if="showLabel(label)"
- @click="filterByLabel(label, $event)"
- :style="labelStyle(label)"
- :title="label.description"
- data-container="body">
- {{ label.title }}
- </button>
- </div>
+ {{ issueId }}
+ </span>
+ </h4>
+ <a
+ class="card-assignee has-tooltip js-no-trigger"
+ :href="assigneeUrl"
+ :title="assigneeUrlTitle"
+ v-if="issue.assignee"
+ data-container="body"
+ >
+ <img
+ class="avatar avatar-inline s20 js-no-trigger"
+ :src="issue.assignee.avatar"
+ width="20"
+ height="20"
+ :alt="avatarUrlTitle"
+ />
+ </a>
+ </div>
+ <div class="card-footer" v-if="showLabelFooter">
+ <button
+ class="label color-label has-tooltip js-no-trigger"
+ v-for="label in issue.labels"
+ type="button"
+ v-if="showLabel(label)"
+ @click="filterByLabel(label, $event)"
+ :style="labelStyle(label)"
+ :title="label.description"
+ data-container="body">
+ {{ label.title }}
+ </button>
</div>
- `,
- });
-})();
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index 823319df6e7..13569df0c20 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -1,71 +1,69 @@
import Vue from 'vue';
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
- gl.issueBoards.ModalEmptyState = Vue.extend({
- mixins: [gl.issueBoards.ModalMixins],
- data() {
- return ModalStore.store;
+gl.issueBoards.ModalEmptyState = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return ModalStore.store;
+ },
+ props: {
+ image: {
+ type: String,
+ required: true,
},
- props: {
- image: {
- type: String,
- required: true,
- },
- newIssuePath: {
- type: String,
- required: true,
- },
+ newIssuePath: {
+ type: String,
+ required: true,
},
- computed: {
- contents() {
- const obj = {
- title: 'You haven\'t added any issues to your project yet',
- content: `
- An issue can be a bug, a todo or a feature request that needs to be
- discussed in a project. Besides, issues are searchable and filterable.
- `,
- };
+ },
+ computed: {
+ contents() {
+ const obj = {
+ title: 'You haven\'t added any issues to your project yet',
+ content: `
+ An issue can be a bug, a todo or a feature request that needs to be
+ discussed in a project. Besides, issues are searchable and filterable.
+ `,
+ };
- if (this.activeTab === 'selected') {
- obj.title = 'You haven\'t selected any issues yet';
- obj.content = `
- Go back to <strong>Open issues</strong> and select some issues
- to add to your board.
- `;
- }
+ if (this.activeTab === 'selected') {
+ obj.title = 'You haven\'t selected any issues yet';
+ obj.content = `
+ Go back to <strong>Open issues</strong> and select some issues
+ to add to your board.
+ `;
+ }
- return obj;
- },
+ return obj;
},
- template: `
- <section class="empty-state">
- <div class="row">
- <div class="col-xs-12 col-sm-6 col-sm-push-6">
- <aside class="svg-content" v-html="image"></aside>
- </div>
- <div class="col-xs-12 col-sm-6 col-sm-pull-6">
- <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>
+ },
+ template: `
+ <section class="empty-state">
+ <div class="row">
+ <div class="col-xs-12 col-sm-6 col-sm-push-6">
+ <aside class="svg-content" v-html="image"></aside>
+ </div>
+ <div class="col-xs-12 col-sm-6 col-sm-pull-6">
+ <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>
</div>
- </section>
- `,
- });
-})();
+ </div>
+ </section>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index 887ce373096..ccd270b27da 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -5,80 +5,78 @@ import Vue from 'vue';
require('./lists_dropdown');
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
- gl.issueBoards.ModalFooter = Vue.extend({
- mixins: [gl.issueBoards.ModalMixins],
- data() {
- return {
- modal: ModalStore.store,
- state: gl.issueBoards.BoardsStore.state,
- };
+gl.issueBoards.ModalFooter = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ submitDisabled() {
+ return !ModalStore.selectedCount();
},
- computed: {
- submitDisabled() {
- return !ModalStore.selectedCount();
- },
- submitText() {
- const count = ModalStore.selectedCount();
+ submitText() {
+ const count = ModalStore.selectedCount();
- return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
- },
+ return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
},
- methods: {
- addIssues() {
- const list = this.modal.selectedList || this.state.lists[0];
- const selectedIssues = ModalStore.getSelectedIssues();
- const issueIds = selectedIssues.map(issue => issue.globalId);
+ },
+ methods: {
+ addIssues() {
+ const list = this.modal.selectedList || this.state.lists[0];
+ const selectedIssues = ModalStore.getSelectedIssues();
+ const issueIds = selectedIssues.map(issue => issue.globalId);
- // Post the data to the backend
- gl.boardService.bulkUpdate(issueIds, {
- add_label_ids: [list.label.id],
- }).catch(() => {
- new Flash('Failed to update issues, please try again.', 'alert');
+ // Post the data to the backend
+ gl.boardService.bulkUpdate(issueIds, {
+ add_label_ids: [list.label.id],
+ }).catch(() => {
+ new Flash('Failed to update issues, please try again.', 'alert');
- selectedIssues.forEach((issue) => {
- list.removeIssue(issue);
- list.issuesSize -= 1;
- });
- });
-
- // Add the issues on the frontend
selectedIssues.forEach((issue) => {
- list.addIssue(issue);
- list.issuesSize += 1;
+ list.removeIssue(issue);
+ list.issuesSize -= 1;
});
+ });
- this.toggleModal(false);
- },
- },
- components: {
- 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
+ // Add the issues on the frontend
+ selectedIssues.forEach((issue) => {
+ list.addIssue(issue);
+ list.issuesSize += 1;
+ });
+
+ this.toggleModal(false);
},
- template: `
- <footer
- class="form-actions add-issues-footer">
- <div class="pull-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>
+ },
+ components: {
+ 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
+ },
+ template: `
+ <footer
+ class="form-actions add-issues-footer">
+ <div class="pull-left">
<button
- class="btn btn-default pull-right"
+ class="btn btn-success"
type="button"
- @click="toggleModal(false)">
- Cancel
+ :disabled="submitDisabled"
+ @click="addIssues">
+ {{ submitText }}
</button>
- </footer>
- `,
- });
-})();
+ <span class="inline add-issues-footer-to-list">
+ to list
+ </span>
+ <lists-dropdown></lists-dropdown>
+ </div>
+ <button
+ class="btn btn-default pull-right"
+ type="button"
+ @click="toggleModal(false)">
+ Cancel
+ </button>
+ </footer>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js
index 116e29cd177..e2b3f9ae7e2 100644
--- a/app/assets/javascripts/boards/components/modal/header.js
+++ b/app/assets/javascripts/boards/components/modal/header.js
@@ -3,80 +3,78 @@ import modalFilters from './filters';
require('./tabs');
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
- gl.issueBoards.ModalHeader = Vue.extend({
- mixins: [gl.issueBoards.ModalMixins],
- props: {
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
+gl.issueBoards.ModalHeader = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
},
- data() {
- return ModalStore.store;
+ milestonePath: {
+ type: String,
+ required: true,
},
- computed: {
- selectAllText() {
- if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
- return 'Select all';
- }
-
- return 'Deselect all';
- },
- showSearch() {
- return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
- },
+ labelPath: {
+ type: String,
+ required: true,
},
- methods: {
- toggleAll() {
- this.$refs.selectAllBtn.blur();
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectAllText() {
+ if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
+ return 'Select all';
+ }
- ModalStore.toggleAll();
- },
+ return 'Deselect all';
+ },
+ showSearch() {
+ return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
- components: {
- 'modal-tabs': gl.issueBoards.ModalTabs,
- modalFilters,
+ },
+ methods: {
+ toggleAll() {
+ this.$refs.selectAllBtn.blur();
+
+ ModalStore.toggleAll();
},
- template: `
- <div>
- <header class="add-issues-header form-actions">
- <h2>
- Add issues
- <button
- type="button"
- class="close"
- data-dismiss="modal"
- aria-label="Close"
- @click="toggleModal(false)">
- <span aria-hidden="true">×</span>
- </button>
- </h2>
- </header>
- <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
- <div
- class="add-issues-search append-bottom-10"
- v-if="showSearch">
- <modal-filters :store="filter" />
+ },
+ components: {
+ 'modal-tabs': gl.issueBoards.ModalTabs,
+ modalFilters,
+ },
+ template: `
+ <div>
+ <header class="add-issues-header form-actions">
+ <h2>
+ Add issues
<button
type="button"
- class="btn btn-success btn-inverted prepend-left-10"
- ref="selectAllBtn"
- @click="toggleAll">
- {{ selectAllText }}
+ class="close"
+ data-dismiss="modal"
+ aria-label="Close"
+ @click="toggleModal(false)">
+ <span aria-hidden="true">×</span>
</button>
- </div>
+ </h2>
+ </header>
+ <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
+ <div
+ class="add-issues-search append-bottom-10"
+ v-if="showSearch">
+ <modal-filters :store="filter" />
+ <button
+ type="button"
+ class="btn btn-success btn-inverted prepend-left-10"
+ ref="selectAllBtn"
+ @click="toggleAll">
+ {{ selectAllText }}
+ </button>
</div>
- `,
- });
-})();
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index 91c08cde13a..fdab317dc23 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -8,160 +8,162 @@ require('./list');
require('./footer');
require('./empty_state');
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
- gl.issueBoards.IssuesModal = Vue.extend({
- props: {
- blankStateImage: {
- type: String,
- required: true,
- },
- newIssuePath: {
- type: String,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
+gl.issueBoards.IssuesModal = Vue.extend({
+ props: {
+ blankStateImage: {
+ type: String,
+ required: true,
},
- data() {
- return ModalStore.store;
+ newIssuePath: {
+ type: String,
+ required: true,
},
- watch: {
- page() {
- this.loadIssues();
- },
- showAddIssuesModal() {
- if (this.showAddIssuesModal && !this.issues.length) {
- this.loading = true;
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ watch: {
+ page() {
+ this.loadIssues();
+ },
+ showAddIssuesModal() {
+ if (this.showAddIssuesModal && !this.issues.length) {
+ this.loading = true;
+ const loadingDone = () => {
+ this.loading = false;
+ };
- this.loadIssues()
- .then(() => {
- this.loading = false;
- });
- } else if (!this.showAddIssuesModal) {
- this.issues = [];
- this.selectedIssues = [];
- this.issuesCount = false;
- }
- },
- filter: {
- handler() {
- if (this.$el.tagName) {
- this.page = 1;
- this.filterLoading = true;
+ this.loadIssues()
+ .then(loadingDone)
+ .catch(loadingDone);
+ } else if (!this.showAddIssuesModal) {
+ this.issues = [];
+ this.selectedIssues = [];
+ this.issuesCount = false;
+ }
+ },
+ filter: {
+ handler() {
+ if (this.$el.tagName) {
+ this.page = 1;
+ this.filterLoading = true;
+ const loadingDone = () => {
+ this.filterLoading = false;
+ };
- this.loadIssues(true)
- .then(() => {
- this.filterLoading = false;
- });
- }
- },
- deep: true,
+ this.loadIssues(true)
+ .then(loadingDone)
+ .catch(loadingDone);
+ }
},
+ deep: true,
},
- methods: {
- loadIssues(clearIssues = false) {
- if (!this.showAddIssuesModal) return false;
-
- return gl.boardService.getBacklog(queryData(this.filter.path, {
- page: this.page,
- per: this.perPage,
- })).then((res) => {
- const data = res.json();
+ },
+ methods: {
+ loadIssues(clearIssues = false) {
+ if (!this.showAddIssuesModal) return false;
- if (clearIssues) {
- this.issues = [];
- }
+ return gl.boardService.getBacklog(queryData(this.filter.path, {
+ page: this.page,
+ per: this.perPage,
+ })).then((res) => {
+ const data = res.json();
- data.issues.forEach((issueObj) => {
- const issue = new ListIssue(issueObj);
- const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
- issue.selected = !!foundSelectedIssue;
-
- this.issues.push(issue);
- });
+ if (clearIssues) {
+ this.issues = [];
+ }
- this.loadingNewPage = false;
+ data.issues.forEach((issueObj) => {
+ const issue = new ListIssue(issueObj);
+ const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
+ issue.selected = !!foundSelectedIssue;
- if (!this.issuesCount) {
- this.issuesCount = data.size;
- }
+ this.issues.push(issue);
});
- },
- },
- computed: {
- showList() {
- if (this.activeTab === 'selected') {
- return this.selectedIssues.length > 0;
- }
- return this.issuesCount > 0;
- },
- showEmptyState() {
- if (!this.loading && this.issuesCount === 0) {
- return true;
- }
+ this.loadingNewPage = false;
- return this.activeTab === 'selected' && this.selectedIssues.length === 0;
- },
+ if (!this.issuesCount) {
+ this.issuesCount = data.size;
+ }
+ });
},
- created() {
- this.page = 1;
+ },
+ computed: {
+ showList() {
+ if (this.activeTab === 'selected') {
+ return this.selectedIssues.length > 0;
+ }
+
+ return this.issuesCount > 0;
},
- components: {
- 'modal-header': gl.issueBoards.ModalHeader,
- 'modal-list': gl.issueBoards.ModalList,
- 'modal-footer': gl.issueBoards.ModalFooter,
- 'empty-state': gl.issueBoards.ModalEmptyState,
+ showEmptyState() {
+ if (!this.loading && this.issuesCount === 0) {
+ return true;
+ }
+
+ return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
- template: `
- <div
- class="add-issues-modal"
- v-if="showAddIssuesModal">
- <div class="add-issues-container">
- <modal-header
- :project-id="projectId"
- :milestone-path="milestonePath"
- :label-path="labelPath">
- </modal-header>
- <modal-list
- :image="blankStateImage"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- v-if="!loading && showList && !filterLoading"></modal-list>
- <empty-state
- v-if="showEmptyState"
- :image="blankStateImage"
- :new-issue-path="newIssuePath"></empty-state>
- <section
- class="add-issues-list text-center"
- v-if="loading || filterLoading">
- <div class="add-issues-list-loading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>
- </section>
- <modal-footer></modal-footer>
- </div>
+ },
+ created() {
+ this.page = 1;
+ },
+ components: {
+ 'modal-header': gl.issueBoards.ModalHeader,
+ 'modal-list': gl.issueBoards.ModalList,
+ 'modal-footer': gl.issueBoards.ModalFooter,
+ 'empty-state': gl.issueBoards.ModalEmptyState,
+ },
+ template: `
+ <div
+ class="add-issues-modal"
+ v-if="showAddIssuesModal">
+ <div class="add-issues-container">
+ <modal-header
+ :project-id="projectId"
+ :milestone-path="milestonePath"
+ :label-path="labelPath">
+ </modal-header>
+ <modal-list
+ :image="blankStateImage"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ v-if="!loading && showList && !filterLoading"></modal-list>
+ <empty-state
+ v-if="showEmptyState"
+ :image="blankStateImage"
+ :new-issue-path="newIssuePath"></empty-state>
+ <section
+ class="add-issues-list text-center"
+ v-if="loading || filterLoading">
+ <div class="add-issues-list-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+ </section>
+ <modal-footer></modal-footer>
</div>
- `,
- });
-})();
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
index aba56d4aa31..363269c0d5d 100644
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ b/app/assets/javascripts/boards/components/modal/list.js
@@ -3,159 +3,157 @@
import Vue from 'vue';
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
- gl.issueBoards.ModalList = Vue.extend({
- props: {
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- image: {
- type: String,
- required: true,
- },
+gl.issueBoards.ModalList = Vue.extend({
+ props: {
+ issueLinkBase: {
+ type: String,
+ required: true,
},
- data() {
- return ModalStore.store;
+ rootPath: {
+ type: String,
+ required: true,
},
- watch: {
- activeTab() {
- if (this.activeTab === 'all') {
- ModalStore.purgeUnselectedIssues();
- }
- },
+ image: {
+ type: String,
+ required: true,
},
- computed: {
- loopIssues() {
- if (this.activeTab === 'all') {
- return this.issues;
- }
-
- return this.selectedIssues;
- },
- groupedIssues() {
- const groups = [];
- this.loopIssues.forEach((issue, i) => {
- const index = i % this.columns;
-
- if (!groups[index]) {
- groups.push([]);
- }
-
- groups[index].push(issue);
- });
+ },
+ data() {
+ return ModalStore.store;
+ },
+ watch: {
+ activeTab() {
+ if (this.activeTab === 'all') {
+ ModalStore.purgeUnselectedIssues();
+ }
+ },
+ },
+ computed: {
+ loopIssues() {
+ if (this.activeTab === 'all') {
+ return this.issues;
+ }
- return groups;
- },
+ return this.selectedIssues;
},
- methods: {
- scrollHandler() {
- const currentPage = Math.floor(this.issues.length / this.perPage);
+ groupedIssues() {
+ const groups = [];
+ this.loopIssues.forEach((issue, i) => {
+ const index = i % this.columns;
- if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
- && currentPage === this.page) {
- this.loadingNewPage = true;
- this.page += 1;
+ if (!groups[index]) {
+ groups.push([]);
}
- },
- toggleIssue(e, issue) {
- if (e.target.tagName !== 'A') {
- ModalStore.toggleIssue(issue);
- }
- },
- listHeight() {
- return this.$refs.list.getBoundingClientRect().height;
- },
- scrollHeight() {
- return this.$refs.list.scrollHeight;
- },
- scrollTop() {
- return this.$refs.list.scrollTop + this.listHeight();
- },
- showIssue(issue) {
- if (this.activeTab === 'all') return true;
-
- const index = ModalStore.selectedIssueIndex(issue);
- return index !== -1;
- },
- setColumnCount() {
- const breakpoint = bp.getBreakpointSize();
+ groups[index].push(issue);
+ });
- if (breakpoint === 'lg' || breakpoint === 'md') {
- this.columns = 3;
- } else if (breakpoint === 'sm') {
- this.columns = 2;
- } else {
- this.columns = 1;
- }
- },
+ return groups;
},
- mounted() {
- this.scrollHandlerWrapper = this.scrollHandler.bind(this);
- this.setColumnCountWrapper = this.setColumnCount.bind(this);
- this.setColumnCount();
+ },
+ methods: {
+ scrollHandler() {
+ const currentPage = Math.floor(this.issues.length / this.perPage);
- this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
- window.addEventListener('resize', this.setColumnCountWrapper);
+ if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
+ && currentPage === this.page) {
+ this.loadingNewPage = true;
+ this.page += 1;
+ }
+ },
+ toggleIssue(e, issue) {
+ if (e.target.tagName !== 'A') {
+ ModalStore.toggleIssue(issue);
+ }
+ },
+ listHeight() {
+ return this.$refs.list.getBoundingClientRect().height;
},
- beforeDestroy() {
- this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
- window.removeEventListener('resize', this.setColumnCountWrapper);
+ scrollHeight() {
+ return this.$refs.list.scrollHeight;
},
- components: {
- 'issue-card-inner': gl.issueBoards.IssueCardInner,
+ scrollTop() {
+ return this.$refs.list.scrollTop + this.listHeight();
},
- template: `
- <section
- class="add-issues-list add-issues-list-columns"
- ref="list">
+ showIssue(issue) {
+ if (this.activeTab === 'all') return true;
+
+ const index = ModalStore.selectedIssueIndex(issue);
+
+ return index !== -1;
+ },
+ setColumnCount() {
+ const breakpoint = bp.getBreakpointSize();
+
+ if (breakpoint === 'lg' || breakpoint === 'md') {
+ this.columns = 3;
+ } else if (breakpoint === 'sm') {
+ this.columns = 2;
+ } else {
+ this.columns = 1;
+ }
+ },
+ },
+ mounted() {
+ this.scrollHandlerWrapper = this.scrollHandler.bind(this);
+ this.setColumnCountWrapper = this.setColumnCount.bind(this);
+ this.setColumnCount();
+
+ this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
+ window.addEventListener('resize', this.setColumnCountWrapper);
+ },
+ beforeDestroy() {
+ this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
+ window.removeEventListener('resize', this.setColumnCountWrapper);
+ },
+ components: {
+ 'issue-card-inner': gl.issueBoards.IssueCardInner,
+ },
+ template: `
+ <section
+ class="add-issues-list add-issues-list-columns"
+ ref="list">
+ <div
+ class="empty-state add-issues-empty-state-filter text-center"
+ v-if="issuesCount > 0 && issues.length === 0">
<div
- class="empty-state add-issues-empty-state-filter text-center"
- v-if="issuesCount > 0 && issues.length === 0">
- <div
- class="svg-content"
- v-html="image">
- </div>
- <div class="text-content">
- <h4>
- There are no issues to show.
- </h4>
- </div>
+ class="svg-content"
+ v-html="image">
+ </div>
+ <div class="text-content">
+ <h4>
+ There are no issues to show.
+ </h4>
</div>
+ </div>
+ <div
+ v-for="group in groupedIssues"
+ class="add-issues-list-column">
<div
- v-for="group in groupedIssues"
- class="add-issues-list-column">
+ v-for="issue in group"
+ v-if="showIssue(issue)"
+ class="card-parent">
<div
- v-for="issue in group"
- v-if="showIssue(issue)"
- class="card-parent">
- <div
- class="card"
- :class="{ 'is-active': issue.selected }"
- @click="toggleIssue($event, issue)">
- <issue-card-inner
- :issue="issue"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath">
- </issue-card-inner>
- <span
- :aria-label="'Issue #' + issue.id + ' selected'"
- aria-checked="true"
- v-if="issue.selected"
- class="issue-card-selected text-center">
- <i class="fa fa-check"></i>
- </span>
- </div>
+ class="card"
+ :class="{ 'is-active': issue.selected }"
+ @click="toggleIssue($event, issue)">
+ <issue-card-inner
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath">
+ </issue-card-inner>
+ <span
+ :aria-label="'Issue #' + issue.id + ' selected'"
+ aria-checked="true"
+ v-if="issue.selected"
+ class="issue-card-selected text-center">
+ <i class="fa fa-check"></i>
+ </span>
</div>
</div>
- </section>
- `,
- });
-})();
+ </div>
+ </section>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
index 9e9ed46ab8d..8cd15df90fa 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
@@ -1,57 +1,55 @@
import Vue from 'vue';
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
- gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
- data() {
- return {
- modal: ModalStore.store,
- state: gl.issueBoards.BoardsStore.state,
- };
+gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ selected() {
+ return this.modal.selectedList || this.state.lists[0];
},
- computed: {
- selected() {
- return this.modal.selectedList || this.state.lists[0];
- },
- },
- 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>
+ },
+ 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/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
index 23cb1b13d11..3e5d08e3d75 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -1,48 +1,46 @@
import Vue from 'vue';
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
- gl.issueBoards.ModalTabs = Vue.extend({
- mixins: [gl.issueBoards.ModalMixins],
- data() {
- return ModalStore.store;
+gl.issueBoards.ModalTabs = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectedCount() {
+ return ModalStore.selectedCount();
},
- 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">
- {{ issuesCount }}
- </span>
- </a>
- </li>
- <li :class="{ 'active': activeTab == 'selected' }">
- <a
- href="#"
- role="button"
- @click.prevent="changeTab('selected')">
- Selected issues
- <span class="badge">
- {{ selectedCount }}
- </span>
- </a>
- </li>
- </ul>
- </div>
- `,
- });
-})();
+ },
+ 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">
+ {{ issuesCount }}
+ </span>
+ </a>
+ </li>
+ <li :class="{ 'active': activeTab == 'selected' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('selected')">
+ Selected issues
+ <span class="badge">
+ {{ selectedCount }}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 556826a9148..7e3bb79af1d 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -1,76 +1,75 @@
-/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */
+/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var,
+ promise/catch-or-return */
-(() => {
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
- const Store = gl.issueBoards.BoardsStore;
+const Store = gl.issueBoards.BoardsStore;
- $(document).off('created.label').on('created.label', (e, label) => {
- Store.new({
+$(document).off('created.label').on('created.label', (e, label) => {
+ Store.new({
+ title: label.title,
+ position: Store.state.lists.length - 2,
+ list_type: 'label',
+ label: {
+ id: label.id,
title: label.title,
- position: Store.state.lists.length - 2,
- list_type: 'label',
- label: {
- id: label.id,
- title: label.title,
- color: label.color
- }
- });
+ color: label.color
+ }
});
+});
- gl.issueBoards.newListDropdownInit = () => {
- $('.js-new-board-list').each(function () {
- const $this = $(this);
- new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
+gl.issueBoards.newListDropdownInit = () => {
+ $('.js-new-board-list').each(function () {
+ const $this = $(this);
+ new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
- $this.glDropdown({
- data(term, callback) {
- $.get($this.attr('data-labels'))
- .then((resp) => {
- callback(resp);
- });
- },
- renderRow (label) {
- const active = Store.findList('title', label.title);
- const $li = $('<li />');
- const $a = $('<a />', {
- class: (active ? `is-active js-board-list-${active.id}` : ''),
- text: label.title,
- href: '#'
- });
- const $labelColor = $('<span />', {
- class: 'dropdown-label-box',
- style: `background-color: ${label.color}`
+ $this.glDropdown({
+ data(term, callback) {
+ $.get($this.attr('data-labels'))
+ .then((resp) => {
+ callback(resp);
});
+ },
+ renderRow (label) {
+ const active = Store.findList('title', label.title);
+ const $li = $('<li />');
+ const $a = $('<a />', {
+ class: (active ? `is-active js-board-list-${active.id}` : ''),
+ text: label.title,
+ href: '#'
+ });
+ const $labelColor = $('<span />', {
+ class: 'dropdown-label-box',
+ style: `background-color: ${label.color}`
+ });
- return $li.append($a.prepend($labelColor));
- },
- search: {
- fields: ['title']
- },
- filterable: true,
- selectable: true,
- multiSelect: true,
- clicked (label, $el, e) {
- e.preventDefault();
+ return $li.append($a.prepend($labelColor));
+ },
+ search: {
+ fields: ['title']
+ },
+ filterable: true,
+ selectable: true,
+ multiSelect: true,
+ clicked (label, $el, e) {
+ e.preventDefault();
- if (!Store.findList('title', label.title)) {
- Store.new({
+ if (!Store.findList('title', label.title)) {
+ Store.new({
+ title: label.title,
+ position: Store.state.lists.length - 2,
+ list_type: 'label',
+ label: {
+ id: label.id,
title: label.title,
- position: Store.state.lists.length - 2,
- list_type: 'label',
- label: {
- id: label.id,
- title: label.title,
- color: label.color
- }
- });
+ color: label.color
+ }
+ });
- Store.state.lists = _.sortBy(Store.state.lists, 'position');
- }
+ Store.state.lists = _.sortBy(Store.state.lists, 'position');
}
- });
+ }
});
- };
-})();
+ });
+};
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index 772ea4c5565..5597f128b80 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -3,59 +3,57 @@
import Vue from 'vue';
-(() => {
- 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,
- },
+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,
},
- methods: {
- removeIssue() {
- const issue = this.issue;
- const lists = issue.getLists();
- const labelIds = lists.map(list => list.label.id);
-
- // Post the remove data
- gl.boardService.bulkUpdate([issue.globalId], {
- remove_label_ids: labelIds,
- }).catch(() => {
- new Flash('Failed to remove issue from board, please try again.', 'alert');
-
- lists.forEach((list) => {
- list.addIssue(issue);
- });
- });
+ list: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ removeIssue() {
+ const issue = this.issue;
+ const lists = issue.getLists();
+ const labelIds = lists.map(list => list.label.id);
+
+ // Post the remove data
+ gl.boardService.bulkUpdate([issue.globalId], {
+ remove_label_ids: labelIds,
+ }).catch(() => {
+ new Flash('Failed to remove issue from board, please try again.', 'alert');
- // Remove from the frontend store
lists.forEach((list) => {
- list.removeIssue(issue);
+ list.addIssue(issue);
});
+ });
+
+ // Remove from the frontend store
+ lists.forEach((list) => {
+ list.removeIssue(issue);
+ });
- Store.detail.issue = {};
- },
+ Store.detail.issue = {};
},
- template: `
- <div
- class="block list"
- v-if="list.type !== 'closed'">
- <button
- class="btn btn-default btn-block"
- type="button"
- @click="removeIssue">
- Remove from board
- </button>
- </div>
- `,
- });
-})();
+ },
+ template: `
+ <div
+ class="block list"
+ v-if="list.type !== 'closed'">
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click="removeIssue">
+ Remove from board
+ </button>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js b/app/assets/javascripts/boards/mixins/modal_mixins.js
index d378b7d4baf..2b0a1aaa89f 100644
--- a/app/assets/javascripts/boards/mixins/modal_mixins.js
+++ b/app/assets/javascripts/boards/mixins/modal_mixins.js
@@ -1,14 +1,12 @@
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
- gl.issueBoards.ModalMixins = {
- methods: {
- toggleModal(toggle) {
- ModalStore.store.showAddIssuesModal = toggle;
- },
- changeTab(tab) {
- ModalStore.store.activeTab = tab;
- },
+gl.issueBoards.ModalMixins = {
+ methods: {
+ toggleModal(toggle) {
+ ModalStore.store.showAddIssuesModal = toggle;
},
- };
-})();
+ changeTab(tab) {
+ ModalStore.store.activeTab = tab;
+ },
+ },
+};
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index b6c6d17274f..38a0eb12f92 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -1,39 +1,37 @@
/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
/* global DocumentTouch */
-((w) => {
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
- gl.issueBoards.onStart = () => {
- $('.has-tooltip').tooltip('hide')
- .tooltip('disable');
- document.body.classList.add('is-dragging');
- };
-
- gl.issueBoards.onEnd = () => {
- $('.has-tooltip').tooltip('enable');
- document.body.classList.remove('is-dragging');
- };
+gl.issueBoards.onStart = () => {
+ $('.has-tooltip').tooltip('hide')
+ .tooltip('disable');
+ document.body.classList.add('is-dragging');
+};
- gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
+gl.issueBoards.onEnd = () => {
+ $('.has-tooltip').tooltip('enable');
+ document.body.classList.remove('is-dragging');
+};
- gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
- const defaultSortOptions = {
- animation: 200,
- forceFallback: true,
- fallbackClass: 'is-dragging',
- fallbackOnBody: true,
- ghostClass: 'is-ghost',
- filter: '.board-delete, .btn',
- delay: gl.issueBoards.touchEnabled ? 100 : 0,
- scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
- scrollSpeed: 20,
- onStart: gl.issueBoards.onStart,
- onEnd: gl.issueBoards.onEnd
- };
+gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
- Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
- return defaultSortOptions;
+gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
+ const defaultSortOptions = {
+ animation: 200,
+ forceFallback: true,
+ fallbackClass: 'is-dragging',
+ fallbackOnBody: true,
+ ghostClass: 'is-ghost',
+ filter: '.board-delete, .btn',
+ delay: gl.issueBoards.touchEnabled ? 100 : 0,
+ scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
+ scrollSpeed: 20,
+ onStart: gl.issueBoards.onStart,
+ onEnd: gl.issueBoards.onEnd
};
-})(window);
+
+ Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
+ return defaultSortOptions;
+};
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 91e5fb2a666..f2b79a88a4a 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -3,6 +3,8 @@
/* global ListLabel */
import queryData from '../utils/query_data';
+const PER_PAGE = 20;
+
class List {
constructor (obj) {
this.id = obj.id;
@@ -58,7 +60,9 @@ class List {
nextPage () {
if (this.issuesSize > this.issues.length) {
- this.page += 1;
+ if (this.issues.length / PER_PAGE >= 1) {
+ this.page += 1;
+ }
return this.getIssues(false);
}
@@ -145,10 +149,7 @@ class List {
}
updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
- gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid)
- .then(() => {
- listFrom.getIssues(false);
- });
+ gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid);
}
findIssue (id) {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index bcda70d0638..ccb00099215 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -3,125 +3,126 @@
import Cookies from 'js-cookie';
-(() => {
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
- gl.issueBoards.BoardsStore = {
- disabled: false,
- filter: {
- path: '',
- },
- state: {},
- detail: {
- issue: {}
- },
- moving: {
- issue: {},
- list: {}
- },
- create () {
- this.state.lists = [];
- this.filter.path = gl.utils.getUrlParamsArray().join('&');
- },
- addList (listObj) {
- const list = new List(listObj);
- this.state.lists.push(list);
+gl.issueBoards.BoardsStore = {
+ disabled: false,
+ filter: {
+ path: '',
+ },
+ state: {},
+ detail: {
+ issue: {}
+ },
+ moving: {
+ issue: {},
+ list: {}
+ },
+ create () {
+ this.state.lists = [];
+ this.filter.path = gl.utils.getUrlParamsArray().join('&');
+ },
+ addList (listObj) {
+ const list = new List(listObj);
+ this.state.lists.push(list);
- return list;
- },
- new (listObj) {
- const list = this.addList(listObj);
+ return list;
+ },
+ new (listObj) {
+ const list = this.addList(listObj);
- list
- .save()
- .then(() => {
- this.state.lists = _.sortBy(this.state.lists, 'position');
- });
- this.removeBlankState();
- },
- updateNewListDropdown (listId) {
- $(`.js-board-list-${listId}`).removeClass('is-active');
- },
- shouldAddBlankState () {
- // Decide whether to add the blank state
- return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
- },
- addBlankState () {
- if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
-
- this.addList({
- id: 'blank',
- list_type: 'blank',
- title: 'Welcome to your Issue Board!',
- position: 0
+ list
+ .save()
+ .then(() => {
+ this.state.lists = _.sortBy(this.state.lists, 'position');
+ })
+ .catch(() => {
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
});
+ this.removeBlankState();
+ },
+ updateNewListDropdown (listId) {
+ $(`.js-board-list-${listId}`).removeClass('is-active');
+ },
+ shouldAddBlankState () {
+ // Decide whether to add the blank state
+ return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
+ },
+ addBlankState () {
+ if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
- this.state.lists = _.sortBy(this.state.lists, 'position');
- },
- removeBlankState () {
- this.removeList('blank');
-
- Cookies.set('issue_board_welcome_hidden', 'true', {
- expires: 365 * 10,
- path: ''
- });
- },
- welcomeIsHidden () {
- return Cookies.get('issue_board_welcome_hidden') === 'true';
- },
- removeList (id, type = 'blank') {
- const list = this.findList('id', id, type);
+ this.addList({
+ id: 'blank',
+ list_type: 'blank',
+ title: 'Welcome to your Issue Board!',
+ position: 0
+ });
- if (!list) return;
+ this.state.lists = _.sortBy(this.state.lists, 'position');
+ },
+ removeBlankState () {
+ this.removeList('blank');
- this.state.lists = this.state.lists.filter(list => list.id !== id);
- },
- moveList (listFrom, orderLists) {
- orderLists.forEach((id, i) => {
- const list = this.findList('id', parseInt(id, 10));
+ Cookies.set('issue_board_welcome_hidden', 'true', {
+ expires: 365 * 10,
+ path: ''
+ });
+ },
+ welcomeIsHidden () {
+ return Cookies.get('issue_board_welcome_hidden') === 'true';
+ },
+ removeList (id, type = 'blank') {
+ const list = this.findList('id', id, type);
- list.position = i;
- });
- listFrom.update();
- },
- moveIssueToList (listFrom, listTo, issue, newIndex) {
- const issueTo = listTo.findIssue(issue.id);
- const issueLists = issue.getLists();
- const listLabels = issueLists.map(listIssue => listIssue.label);
+ if (!list) return;
- if (!issueTo) {
- // Add to new lists issues if it doesn't already exist
- listTo.addIssue(issue, listFrom, newIndex);
- } else {
- listTo.updateIssueLabel(issue, listFrom);
- issueTo.removeLabel(listFrom.label);
- }
+ this.state.lists = this.state.lists.filter(list => list.id !== id);
+ },
+ moveList (listFrom, orderLists) {
+ orderLists.forEach((id, i) => {
+ const list = this.findList('id', parseInt(id, 10));
- if (listTo.type === 'closed') {
- issueLists.forEach((list) => {
- list.removeIssue(issue);
- });
- issue.removeLabels(listLabels);
- } else {
- listFrom.removeIssue(issue);
- }
- },
- moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
- const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
- const afterId = parseInt(idArray[newIndex + 1], 10) || null;
+ list.position = i;
+ });
+ listFrom.update();
+ },
+ moveIssueToList (listFrom, listTo, issue, newIndex) {
+ const issueTo = listTo.findIssue(issue.id);
+ const issueLists = issue.getLists();
+ const listLabels = issueLists.map(listIssue => listIssue.label);
- list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
- },
- findList (key, val, type = 'label') {
- return this.state.lists.filter((list) => {
- const byType = type ? list['type'] === type : true;
+ if (!issueTo) {
+ // Add to new lists issues if it doesn't already exist
+ listTo.addIssue(issue, listFrom, newIndex);
+ } else {
+ listTo.updateIssueLabel(issue, listFrom);
+ issueTo.removeLabel(listFrom.label);
+ }
- return list[key] === val && byType;
- })[0];
- },
- updateFiltersUrl () {
- history.pushState(null, null, `?${this.filter.path}`);
+ if (listTo.type === 'closed') {
+ issueLists.forEach((list) => {
+ list.removeIssue(issue);
+ });
+ issue.removeLabels(listLabels);
+ } else {
+ listFrom.removeIssue(issue);
}
- };
-})();
+ },
+ moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
+ const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
+ const afterId = parseInt(idArray[newIndex + 1], 10) || null;
+
+ list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
+ },
+ findList (key, val, type = 'label') {
+ return this.state.lists.filter((list) => {
+ const byType = type ? list['type'] === type : true;
+
+ return list[key] === val && byType;
+ })[0];
+ },
+ updateFiltersUrl () {
+ history.pushState(null, null, `?${this.filter.path}`);
+ }
+};
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index 9b009483a3c..4fdc925c825 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -1,100 +1,98 @@
-(() => {
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- class ModalStore {
- constructor() {
- this.store = {
- columns: 3,
- issues: [],
- issuesCount: false,
- selectedIssues: [],
- showAddIssuesModal: false,
- activeTab: 'all',
- selectedList: null,
- searchTerm: '',
- loading: false,
- loadingNewPage: false,
- filterLoading: false,
- page: 1,
- perPage: 50,
- filter: {
- path: '',
- },
- };
- }
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
+
+class ModalStore {
+ constructor() {
+ this.store = {
+ columns: 3,
+ issues: [],
+ issuesCount: false,
+ selectedIssues: [],
+ showAddIssuesModal: false,
+ activeTab: 'all',
+ selectedList: null,
+ searchTerm: '',
+ loading: false,
+ loadingNewPage: false,
+ filterLoading: false,
+ page: 1,
+ perPage: 50,
+ filter: {
+ path: '',
+ },
+ };
+ }
- selectedCount() {
- return this.getSelectedIssues().length;
- }
+ selectedCount() {
+ return this.getSelectedIssues().length;
+ }
- toggleIssue(issueObj) {
- const issue = issueObj;
- const selected = issue.selected;
+ toggleIssue(issueObj) {
+ const issue = issueObj;
+ const selected = issue.selected;
- issue.selected = !selected;
+ issue.selected = !selected;
- if (!selected) {
- this.addSelectedIssue(issue);
- } else {
- this.removeSelectedIssue(issue);
- }
+ if (!selected) {
+ this.addSelectedIssue(issue);
+ } else {
+ this.removeSelectedIssue(issue);
}
+ }
- toggleAll() {
- const select = this.selectedCount() !== this.store.issues.length;
+ toggleAll() {
+ const select = this.selectedCount() !== this.store.issues.length;
- this.store.issues.forEach((issue) => {
- const issueUpdate = issue;
+ this.store.issues.forEach((issue) => {
+ const issueUpdate = issue;
- if (issueUpdate.selected !== select) {
- issueUpdate.selected = select;
+ if (issueUpdate.selected !== select) {
+ issueUpdate.selected = select;
- if (select) {
- this.addSelectedIssue(issue);
- } else {
- this.removeSelectedIssue(issue);
- }
+ if (select) {
+ this.addSelectedIssue(issue);
+ } else {
+ this.removeSelectedIssue(issue);
}
- });
- }
+ }
+ });
+ }
- getSelectedIssues() {
- return this.store.selectedIssues.filter(issue => issue.selected);
- }
+ getSelectedIssues() {
+ return this.store.selectedIssues.filter(issue => issue.selected);
+ }
- addSelectedIssue(issue) {
- const index = this.selectedIssueIndex(issue);
+ addSelectedIssue(issue) {
+ const index = this.selectedIssueIndex(issue);
- if (index === -1) {
- this.store.selectedIssues.push(issue);
- }
+ if (index === -1) {
+ this.store.selectedIssues.push(issue);
}
+ }
- removeSelectedIssue(issue, forcePurge = false) {
- if (this.store.activeTab === 'all' || forcePurge) {
- this.store.selectedIssues = this.store.selectedIssues
- .filter(fIssue => fIssue.id !== issue.id);
- }
+ removeSelectedIssue(issue, forcePurge = false) {
+ if (this.store.activeTab === 'all' || forcePurge) {
+ this.store.selectedIssues = this.store.selectedIssues
+ .filter(fIssue => fIssue.id !== issue.id);
}
+ }
- purgeUnselectedIssues() {
- this.store.selectedIssues.forEach((issue) => {
- if (!issue.selected) {
- this.removeSelectedIssue(issue, true);
- }
- });
- }
+ purgeUnselectedIssues() {
+ this.store.selectedIssues.forEach((issue) => {
+ if (!issue.selected) {
+ this.removeSelectedIssue(issue, true);
+ }
+ });
+ }
- selectedIssueIndex(issue) {
- return this.store.selectedIssues.indexOf(issue);
- }
+ selectedIssueIndex(issue) {
+ return this.store.selectedIssues.indexOf(issue);
+ }
- findSelectedIssue(issue) {
- return this.store.selectedIssues
- .filter(filteredIssue => filteredIssue.id === issue.id)[0];
- }
+ findSelectedIssue(issue) {
+ return this.store.selectedIssues
+ .filter(filteredIssue => filteredIssue.id === issue.id)[0];
}
+}
- gl.issueBoards.ModalStore = new ModalStore();
-})();
+gl.issueBoards.ModalStore = new ModalStore();
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 9e7df9ad350..97f279e4be4 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -2,6 +2,8 @@
consistent-return, prefer-rest-params */
/* global Breakpoints */
+import { bytesToKiB } from './lib/utils/number_utils';
+
const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; };
const AUTO_SCROLL_OFFSET = 75;
const DOWN_BUILD_TRACE = '#down-build-trace';
@@ -20,6 +22,7 @@ window.Build = (function () {
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
this.$document = $(document);
+ this.logBytes = 0;
this.updateDropdown = bind(this.updateDropdown, this);
@@ -90,21 +93,30 @@ window.Build = (function () {
success: ((log) => {
const $buildContainer = $('.js-build-output');
+ gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
+
if (log.state) {
this.state = log.state;
}
if (log.append) {
$buildContainer.append(log.html);
+ this.logBytes += log.size;
} else {
$buildContainer.html(log.html);
- if (log.truncated) {
- $('.js-truncated-info-size').html(` ${log.size} `);
- this.$truncatedInfo.removeClass('hidden');
- this.initAffixTruncatedInfo();
- } else {
- this.$truncatedInfo.addClass('hidden');
- }
+ this.logBytes = log.size;
+ }
+
+ // if the incremental sum of logBytes we received is less than the total
+ // we need to show a message warning the user about that.
+ if (this.logBytes < log.total) {
+ // size is in bytes, we need to calculate KiB
+ const size = bytesToKiB(this.logBytes);
+ $('.js-truncated-info-size').html(`${size}`);
+ this.$truncatedInfo.removeClass('hidden');
+ this.initAffixTruncatedInfo();
+ } else {
+ this.$truncatedInfo.addClass('hidden');
}
this.checkAutoscroll();
diff --git a/app/assets/javascripts/ci_status_icons.js b/app/assets/javascripts/ci_status_icons.js
new file mode 100644
index 00000000000..f16616873b2
--- /dev/null
+++ b/app/assets/javascripts/ci_status_icons.js
@@ -0,0 +1,34 @@
+import CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
+import CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
+import FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
+import MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
+import PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
+import RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
+import SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
+import SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
+import WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
+
+const StatusIconEntityMap = {
+ icon_status_canceled: CANCELED_SVG,
+ icon_status_created: CREATED_SVG,
+ icon_status_failed: FAILED_SVG,
+ icon_status_manual: MANUAL_SVG,
+ icon_status_pending: PENDING_SVG,
+ icon_status_running: RUNNING_SVG,
+ icon_status_skipped: SKIPPED_SVG,
+ icon_status_success: SUCCESS_SVG,
+ icon_status_warning: WARNING_SVG,
+};
+
+export {
+ CANCELED_SVG,
+ CREATED_SVG,
+ FAILED_SVG,
+ MANUAL_SVG,
+ PENDING_SVG,
+ RUNNING_SVG,
+ SKIPPED_SVG,
+ SUCCESS_SVG,
+ WARNING_SVG,
+ StatusIconEntityMap as default,
+};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index da9707549f9..68a1c1de1df 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
-import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
-import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
-import eventHub from '../../vue_pipelines_index/event_hub';
-import EmptyState from '../../vue_pipelines_index/components/empty_state';
-import ErrorState from '../../vue_pipelines_index/components/error_state';
+import PipelinesService from '../../pipelines/services/pipelines_service';
+import PipelineStore from '../../pipelines/stores/pipelines_store';
+import eventHub from '../../pipelines/event_hub';
+import EmptyState from '../../pipelines/components/empty_state.vue';
+import ErrorState from '../../pipelines/components/error_state.vue';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
import Poll from '../../lib/utils/poll';
@@ -55,7 +55,15 @@ export default Vue.component('pipelines-table', {
},
shouldRenderEmptyState() {
- return !this.state.pipelines.length && !this.isLoading;
+ return !this.state.pipelines.length &&
+ !this.isLoading &&
+ !this.hasError;
+ },
+
+ shouldRenderTable() {
+ return !this.isLoading &&
+ this.state.pipelines.length > 0 &&
+ !this.hasError;
},
},
@@ -145,8 +153,12 @@ export default Vue.component('pipelines-table', {
template: `
<div class="content-list pipelines">
- <div class="realtime-loading" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
+ <div
+ class="realtime-loading"
+ v-if="isLoading">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
</div>
<empty-state
@@ -155,8 +167,9 @@ export default Vue.component('pipelines-table', {
<error-state v-if="shouldRenderErrorState" />
- <div class="table-holder"
- v-if="!isLoading && state.pipelines.length > 0">
+ <div
+ class="table-holder"
+ v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service" />
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 3253eebd9b5..cb054a2a197 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -1,6 +1,7 @@
// ECMAScript polyfills
import 'core-js/fn/array/find';
import 'core-js/fn/array/from';
+import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
index 3f419a96ff9..80bd2df6f42 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
@@ -2,46 +2,45 @@
import Vue from 'vue';
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
- global.cycleAnalytics.StageCodeComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="mergeRequest in items" class="stage-event-item">
- <div class="item-details">
- <img class="avatar" :src="mergeRequest.author.avatarUrl">
- <h5 class="item-title merge-merquest-title">
- <a :href="mergeRequest.url">
- {{ mergeRequest.title }}
- </a>
- </h5>
- <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
- &middot;
- <span>
- Opened
- <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
- </span>
- <span>
- by
- <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="mergeRequest.totalTime"></total-time>
- </div>
- </li>
- </ul>
+global.cycleAnalytics.StageCodeComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ <ul class="stage-event-list">
+ <li v-for="mergeRequest in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="mergeRequest.author.avatarUrl">
+ <h5 class="item-title merge-merquest-title">
+ <a :href="mergeRequest.url">
+ {{ mergeRequest.title }}
+ </a>
+ </h5>
+ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="mergeRequest.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
index 7ffa38edd9e..20a43798fbe 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
@@ -2,48 +2,47 @@
import Vue from 'vue';
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
- global.cycleAnalytics.StageIssueComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="issue in items" class="stage-event-item">
- <div class="item-details">
- <img class="avatar" :src="issue.author.avatarUrl">
- <h5 class="item-title issue-title">
- <a class="issue-title" :href="issue.url">
- {{ issue.title }}
- </a>
- </h5>
- <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
- &middot;
- <span>
- Opened
- <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
- </span>
- <span>
- by
- <a :href="issue.author.webUrl" class="issue-author-link">
- {{ issue.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="issue.totalTime"></total-time>
- </div>
- </li>
- </ul>
+global.cycleAnalytics.StageIssueComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ <ul class="stage-event-list">
+ <li v-for="issue in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="issue.author.avatarUrl">
+ <h5 class="item-title issue-title">
+ <a class="issue-title" :href="issue.url">
+ {{ issue.title }}
+ </a>
+ </h5>
+ <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="issue.author.webUrl" class="issue-author-link">
+ {{ issue.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="issue.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
index d736c8b0c28..f33cac3da82 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
@@ -2,50 +2,49 @@
import Vue from 'vue';
import iconCommit from '../svg/icon_commit.svg';
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
- global.cycleAnalytics.StagePlanComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
+global.cycleAnalytics.StagePlanComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
- data() {
- return { iconCommit };
- },
+ data() {
+ return { iconCommit };
+ },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="commit in items" class="stage-event-item">
- <div class="item-details item-conmmit-component">
- <img class="avatar" :src="commit.author.avatarUrl">
- <h5 class="item-title commit-title">
- <a :href="commit.commitUrl">
- {{ commit.title }}
- </a>
- </h5>
- <span>
- First
- <span class="commit-icon">${iconCommit}</span>
- <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
- pushed by
- <a :href="commit.author.webUrl" class="commit-author-link">
- {{ commit.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="commit.totalTime"></total-time>
- </div>
- </li>
- </ul>
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ <ul class="stage-event-list">
+ <li v-for="commit in items" class="stage-event-item">
+ <div class="item-details item-conmmit-component">
+ <img class="avatar" :src="commit.author.avatarUrl">
+ <h5 class="item-title commit-title">
+ <a :href="commit.commitUrl">
+ {{ commit.title }}
+ </a>
+ </h5>
+ <span>
+ First
+ <span class="commit-icon">${iconCommit}</span>
+ <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
+ pushed by
+ <a :href="commit.author.webUrl" class="commit-author-link">
+ {{ commit.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="commit.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
index 698a79ca68c..657f5385374 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
@@ -2,48 +2,47 @@
import Vue from 'vue';
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
- global.cycleAnalytics.StageProductionComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="issue in items" class="stage-event-item">
- <div class="item-details">
- <img class="avatar" :src="issue.author.avatarUrl">
- <h5 class="item-title issue-title">
- <a class="issue-title" :href="issue.url">
- {{ issue.title }}
- </a>
- </h5>
- <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
- &middot;
- <span>
- Opened
- <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
- </span>
- <span>
- by
- <a :href="issue.author.webUrl" class="issue-author-link">
- {{ issue.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="issue.totalTime"></total-time>
- </div>
- </li>
- </ul>
+global.cycleAnalytics.StageProductionComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ <ul class="stage-event-list">
+ <li v-for="issue in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="issue.author.avatarUrl">
+ <h5 class="item-title issue-title">
+ <a class="issue-title" :href="issue.url">
+ {{ issue.title }}
+ </a>
+ </h5>
+ <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="issue.author.webUrl" class="issue-author-link">
+ {{ issue.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="issue.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
index e63c41f2a57..8a801300647 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
@@ -2,58 +2,57 @@
import Vue from 'vue';
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
- global.cycleAnalytics.StageReviewComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="mergeRequest in items" class="stage-event-item">
- <div class="item-details">
- <img class="avatar" :src="mergeRequest.author.avatarUrl">
- <h5 class="item-title merge-merquest-title">
- <a :href="mergeRequest.url">
- {{ mergeRequest.title }}
- </a>
- </h5>
- <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
- &middot;
- <span>
- Opened
- <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+global.cycleAnalytics.StageReviewComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="mergeRequest in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="mergeRequest.author.avatarUrl">
+ <h5 class="item-title merge-merquest-title">
+ <a :href="mergeRequest.url">
+ {{ mergeRequest.title }}
+ </a>
+ </h5>
+ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+ </span>
+ <template v-if="mergeRequest.state === 'closed'">
+ <span class="merge-request-state">
+ <i class="fa fa-ban"></i>
+ {{ mergeRequest.state.toUpperCase() }}
</span>
- <span>
- by
- <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+ </template>
+ <template v-else>
+ <span class="merge-request-branch" v-if="mergeRequest.branch">
+ <i class= "fa fa-code-fork"></i>
+ <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
- <template v-if="mergeRequest.state === 'closed'">
- <span class="merge-request-state">
- <i class="fa fa-ban"></i>
- {{ mergeRequest.state.toUpperCase() }}
- </span>
- </template>
- <template v-else>
- <span class="merge-request-branch" v-if="mergeRequest.branch">
- <i class= "fa fa-code-fork"></i>
- <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
- </span>
- </template>
- </div>
- <div class="item-time">
- <total-time :time="mergeRequest.totalTime"></total-time>
- </div>
- </li>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ </template>
+ </div>
+ <div class="item-time">
+ <total-time :time="mergeRequest.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
index d51f7134e25..4a286379588 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
@@ -2,48 +2,47 @@
import Vue from 'vue';
import iconBranch from '../svg/icon_branch.svg';
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
- global.cycleAnalytics.StageStagingComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- data() {
- return { iconBranch };
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="build in items" class="stage-event-item item-build-component">
- <div class="item-details">
- <img class="avatar" :src="build.author.avatarUrl">
- <h5 class="item-title">
- <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
- <i class="fa fa-code-fork"></i>
- <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
- <span class="icon-branch">${iconBranch}</span>
- <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
- </h5>
- <span>
- <a :href="build.url" class="build-date">{{ build.date }}</a>
- by
- <a :href="build.author.webUrl" class="issue-author-link">
- {{ build.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="build.totalTime"></total-time>
- </div>
- </li>
- </ul>
+global.cycleAnalytics.StageStagingComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ data() {
+ return { iconBranch };
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ <ul class="stage-event-list">
+ <li v-for="build in items" class="stage-event-item item-build-component">
+ <div class="item-details">
+ <img class="avatar" :src="build.author.avatarUrl">
+ <h5 class="item-title">
+ <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+ <i class="fa fa-code-fork"></i>
+ <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+ <span class="icon-branch">${iconBranch}</span>
+ <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+ </h5>
+ <span>
+ <a :href="build.url" class="build-date">{{ build.date }}</a>
+ by
+ <a :href="build.author.webUrl" class="issue-author-link">
+ {{ build.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="build.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
index 17ae3a9ddc1..e306026429e 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
@@ -3,48 +3,47 @@ import Vue from 'vue';
import iconBuildStatus from '../svg/icon_build_status.svg';
import iconBranch from '../svg/icon_branch.svg';
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
- global.cycleAnalytics.StageTestComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- data() {
- return { iconBuildStatus, iconBranch };
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="build in items" class="stage-event-item item-build-component">
- <div class="item-details">
- <h5 class="item-title">
- <span class="icon-build-status">${iconBuildStatus}</span>
- <a :href="build.url" class="item-build-name">{{ build.name }}</a>
- &middot;
- <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
- <i class="fa fa-code-fork"></i>
- <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
- <span class="icon-branch">${iconBranch}</span>
- <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
- </h5>
- <span>
- <a :href="build.url" class="issue-date">
- {{ build.date }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="build.totalTime"></total-time>
- </div>
- </li>
- </ul>
+global.cycleAnalytics.StageTestComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ data() {
+ return { iconBuildStatus, iconBranch };
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ <ul class="stage-event-list">
+ <li v-for="build in items" class="stage-event-item item-build-component">
+ <div class="item-details">
+ <h5 class="item-title">
+ <span class="icon-build-status">${iconBuildStatus}</span>
+ <a :href="build.url" class="item-build-name">{{ build.name }}</a>
+ &middot;
+ <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+ <i class="fa fa-code-fork"></i>
+ <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+ <span class="icon-branch">${iconBranch}</span>
+ <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+ </h5>
+ <span>
+ <a :href="build.url" class="issue-date">
+ {{ build.date }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="build.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
index b4442ea5566..77edcb76273 100644
--- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
@@ -2,25 +2,24 @@
import Vue from 'vue';
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
- global.cycleAnalytics.TotalTimeComponent = Vue.extend({
- props: {
- time: Object,
- },
- template: `
- <span class="total-time">
- <template v-if="Object.keys(time).length">
- <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
- <template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
- <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
- <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
- </template>
- <template v-else>
- --
- </template>
- </span>
- `,
- });
-})(window.gl || (window.gl = {}));
+global.cycleAnalytics.TotalTimeComponent = Vue.extend({
+ props: {
+ time: Object,
+ },
+ template: `
+ <span class="total-time">
+ <template v-if="Object.keys(time).length">
+ <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
+ <template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
+ <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
+ <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
+ </template>
+ <template v-else>
+ --
+ </template>
+ </span>
+ `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index b099b39e58f..48cab437e02 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -125,7 +125,7 @@ $(() => {
},
dismissOverviewDialog() {
this.isOverviewDialogDismissed = true;
- Cookies.set(OVERVIEW_DIALOG_COOKIE, '1');
+ Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
},
},
});
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
index 9f74b14c4b9..681d6eef565 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
@@ -1,41 +1,41 @@
/* eslint-disable no-param-reassign */
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
- class CycleAnalyticsService {
- constructor(options) {
- this.requestPath = options.requestPath;
- }
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
- fetchCycleAnalyticsData(options) {
- options = options || { startDate: 30 };
-
- return $.ajax({
- url: this.requestPath,
- method: 'GET',
- dataType: 'json',
- contentType: 'application/json',
- data: {
- cycle_analytics: {
- start_date: options.startDate,
- },
- },
- });
- }
+class CycleAnalyticsService {
+ constructor(options) {
+ this.requestPath = options.requestPath;
+ }
- fetchStageData(options) {
- const {
- stage,
- startDate,
- } = options;
+ fetchCycleAnalyticsData(options) {
+ options = options || { startDate: 30 };
- return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
+ return $.ajax({
+ url: this.requestPath,
+ method: 'GET',
+ dataType: 'json',
+ contentType: 'application/json',
+ data: {
cycle_analytics: {
- start_date: startDate,
+ start_date: options.startDate,
},
- });
- }
+ },
+ });
+ }
+
+ fetchStageData(options) {
+ const {
+ stage,
+ startDate,
+ } = options;
+
+ return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
+ cycle_analytics: {
+ start_date: startDate,
+ },
+ });
}
+}
- global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
-})(window.gl || (window.gl = {}));
+global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 7ae9de7297c..6536a8fd7fa 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -3,102 +3,101 @@
require('../lib/utils/text_utility');
const DEFAULT_EVENT_OBJECTS = require('./default_event_objects');
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
- const EMPTY_STAGE_TEXTS = {
- issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
- plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
- code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
- test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
- review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
- staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
- production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
- };
+const EMPTY_STAGE_TEXTS = {
+ issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
+ plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
+ code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
+ test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
+ review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
+ staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
+ production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
+};
- global.cycleAnalytics.CycleAnalyticsStore = {
- state: {
- summary: '',
- stats: '',
- analytics: '',
- events: [],
- stages: [],
- },
- setCycleAnalyticsData(data) {
- this.state = Object.assign(this.state, this.decorateData(data));
- },
- decorateData(data) {
- const newData = {};
+global.cycleAnalytics.CycleAnalyticsStore = {
+ state: {
+ summary: '',
+ stats: '',
+ analytics: '',
+ events: [],
+ stages: [],
+ },
+ setCycleAnalyticsData(data) {
+ this.state = Object.assign(this.state, this.decorateData(data));
+ },
+ decorateData(data) {
+ const newData = {};
- newData.stages = data.stats || [];
- newData.summary = data.summary || [];
+ newData.stages = data.stats || [];
+ newData.summary = data.summary || [];
- newData.summary.forEach((item) => {
- item.value = item.value || '-';
- });
+ newData.summary.forEach((item) => {
+ item.value = item.value || '-';
+ });
- newData.stages.forEach((item) => {
- const stageSlug = gl.text.dasherize(item.title.toLowerCase());
- item.active = false;
- item.isUserAllowed = data.permissions[stageSlug];
- item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
- item.component = `stage-${stageSlug}-component`;
- item.slug = stageSlug;
- });
- newData.analytics = data;
- return newData;
- },
- setLoadingState(state) {
- this.state.isLoading = state;
- },
- setErrorState(state) {
- this.state.hasError = state;
- },
- deactivateAllStages() {
- this.state.stages.forEach((stage) => {
- stage.active = false;
- });
- },
- setActiveStage(stage) {
- this.deactivateAllStages();
- stage.active = true;
- },
- setStageEvents(events, stage) {
- this.state.events = this.decorateEvents(events, stage);
- },
- decorateEvents(events, stage) {
- const newEvents = [];
+ newData.stages.forEach((item) => {
+ const stageSlug = gl.text.dasherize(item.title.toLowerCase());
+ item.active = false;
+ item.isUserAllowed = data.permissions[stageSlug];
+ item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
+ item.component = `stage-${stageSlug}-component`;
+ item.slug = stageSlug;
+ });
+ newData.analytics = data;
+ return newData;
+ },
+ setLoadingState(state) {
+ this.state.isLoading = state;
+ },
+ setErrorState(state) {
+ this.state.hasError = state;
+ },
+ deactivateAllStages() {
+ this.state.stages.forEach((stage) => {
+ stage.active = false;
+ });
+ },
+ setActiveStage(stage) {
+ this.deactivateAllStages();
+ stage.active = true;
+ },
+ setStageEvents(events, stage) {
+ this.state.events = this.decorateEvents(events, stage);
+ },
+ decorateEvents(events, stage) {
+ const newEvents = [];
- events.forEach((item) => {
- if (!item) return;
+ events.forEach((item) => {
+ if (!item) return;
- const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
+ const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
- eventItem.totalTime = eventItem.total_time;
+ eventItem.totalTime = eventItem.total_time;
- if (eventItem.author) {
- eventItem.author.webUrl = eventItem.author.web_url;
- eventItem.author.avatarUrl = eventItem.author.avatar_url;
- }
+ if (eventItem.author) {
+ eventItem.author.webUrl = eventItem.author.web_url;
+ eventItem.author.avatarUrl = eventItem.author.avatar_url;
+ }
- if (eventItem.created_at) eventItem.createdAt = eventItem.created_at;
- if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha;
- if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url;
+ if (eventItem.created_at) eventItem.createdAt = eventItem.created_at;
+ if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha;
+ if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url;
- delete eventItem.author.web_url;
- delete eventItem.author.avatar_url;
- delete eventItem.total_time;
- delete eventItem.created_at;
- delete eventItem.short_sha;
- delete eventItem.commit_url;
+ delete eventItem.author.web_url;
+ delete eventItem.author.avatar_url;
+ delete eventItem.total_time;
+ delete eventItem.created_at;
+ delete eventItem.short_sha;
+ delete eventItem.commit_url;
- newEvents.push(eventItem);
- });
+ newEvents.push(eventItem);
+ });
- return newEvents;
- },
- currentActiveStage() {
- return this.state.stages.find(stage => stage.active);
- },
- };
-})(window.gl || (window.gl = {}));
+ return newEvents;
+ },
+ currentActiveStage() {
+ return this.state.stages.find(stage => stage.active);
+ },
+};
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
index eb76b7d15fd..aed7cac4e62 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
@@ -3,65 +3,63 @@
import Vue from 'vue';
-(() => {
- const CommentAndResolveBtn = Vue.extend({
- props: {
- discussionId: String,
+const CommentAndResolveBtn = Vue.extend({
+ props: {
+ discussionId: String,
+ },
+ data() {
+ return {
+ textareaIsEmpty: true,
+ discussion: {},
+ };
+ },
+ computed: {
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
},
- data() {
- return {
- textareaIsEmpty: true,
- discussion: {},
- };
+ isDiscussionResolved: function () {
+ return this.discussion.isResolved();
},
- computed: {
- showButton: function () {
- if (this.discussion) {
- return this.discussion.isResolvable();
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ if (this.textareaIsEmpty) {
+ return "Unresolve discussion";
} else {
- return false;
+ return "Comment & unresolve discussion";
}
- },
- isDiscussionResolved: function () {
- return this.discussion.isResolved();
- },
- buttonText: function () {
- if (this.isDiscussionResolved) {
- if (this.textareaIsEmpty) {
- return "Unresolve discussion";
- } else {
- return "Comment & unresolve discussion";
- }
+ } else {
+ if (this.textareaIsEmpty) {
+ return "Resolve discussion";
} else {
- if (this.textareaIsEmpty) {
- return "Resolve discussion";
- } else {
- return "Comment & resolve discussion";
- }
+ return "Comment & resolve discussion";
}
}
- },
- created() {
- if (this.discussionId) {
- this.discussion = CommentsStore.state[this.discussionId];
- }
- },
- mounted: function () {
- if (!this.discussionId) return;
+ }
+ },
+ created() {
+ if (this.discussionId) {
+ this.discussion = CommentsStore.state[this.discussionId];
+ }
+ },
+ mounted: function () {
+ if (!this.discussionId) return;
- const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`);
- this.textareaIsEmpty = $textarea.val() === '';
+ const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`);
+ this.textareaIsEmpty = $textarea.val() === '';
- $textarea.on('input.comment-and-resolve-btn', () => {
- this.textareaIsEmpty = $textarea.val() === '';
- });
- },
- destroyed: function () {
- if (!this.discussionId) return;
+ $textarea.on('input.comment-and-resolve-btn', () => {
+ this.textareaIsEmpty = $textarea.val() === '';
+ });
+ },
+ destroyed: function () {
+ if (!this.discussionId) return;
- $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn');
- }
- });
+ $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn');
+ }
+});
- Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
-})(window);
+Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
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 0297add94d5..f3a688fbf2f 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -4,155 +4,153 @@
import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg';
-(() => {
- const DiffNoteAvatars = Vue.extend({
- props: ['discussionId'],
- data() {
- return {
- isVisible: false,
- lineType: '',
- storeState: CommentsStore.state,
- shownAvatars: 3,
- collapseIcon,
- };
- },
- template: `
- <div class="diff-comment-avatar-holders"
- v-show="notesCount !== 0">
- <div v-if="!isVisible">
- <img v-for="note in notesSubset"
- class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
- width="19"
- height="19"
- role="button"
- data-container="body"
- data-placement="top"
- data-html="true"
- :data-line-type="lineType"
- :title="note.authorName + ': ' + note.noteTruncated"
- :src="note.authorAvatar"
- @click="clickedAvatar($event)" />
- <span v-if="notesCount > shownAvatars"
- class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
- data-container="body"
- data-placement="top"
- ref="extraComments"
- role="button"
- :data-line-type="lineType"
- :title="extraNotesTitle"
- @click="clickedAvatar($event)">{{ moreText }}</span>
- </div>
- <button class="diff-notes-collapse js-diff-comment-avatar"
- type="button"
- aria-label="Show comments"
+const DiffNoteAvatars = Vue.extend({
+ props: ['discussionId'],
+ data() {
+ return {
+ isVisible: false,
+ lineType: '',
+ storeState: CommentsStore.state,
+ shownAvatars: 3,
+ collapseIcon,
+ };
+ },
+ template: `
+ <div class="diff-comment-avatar-holders"
+ v-show="notesCount !== 0">
+ <div v-if="!isVisible">
+ <img v-for="note in notesSubset"
+ class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
+ width="19"
+ height="19"
+ role="button"
+ data-container="body"
+ data-placement="top"
+ data-html="true"
+ :data-line-type="lineType"
+ :title="note.authorName + ': ' + note.noteTruncated"
+ :src="note.authorAvatar"
+ @click="clickedAvatar($event)" />
+ <span v-if="notesCount > shownAvatars"
+ class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
+ data-container="body"
+ data-placement="top"
+ ref="extraComments"
+ role="button"
:data-line-type="lineType"
- @click="clickedAvatar($event)"
- v-if="isVisible"
- v-html="collapseIcon">
- </button>
+ :title="extraNotesTitle"
+ @click="clickedAvatar($event)">{{ moreText }}</span>
</div>
- `,
- mounted() {
+ <button class="diff-notes-collapse js-diff-comment-avatar"
+ type="button"
+ aria-label="Show comments"
+ :data-line-type="lineType"
+ @click="clickedAvatar($event)"
+ v-if="isVisible"
+ v-html="collapseIcon">
+ </button>
+ </div>
+ `,
+ mounted() {
+ this.$nextTick(() => {
+ this.addNoCommentClass();
+ this.setDiscussionVisible();
+
+ this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
+ });
+
+ $(document).on('toggle.comments', () => {
this.$nextTick(() => {
- this.addNoCommentClass();
this.setDiscussionVisible();
-
- this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
});
-
- $(document).on('toggle.comments', () => {
+ });
+ },
+ destroyed() {
+ $(document).off('toggle.comments');
+ },
+ watch: {
+ storeState: {
+ handler() {
this.$nextTick(() => {
- this.setDiscussionVisible();
+ $('.has-tooltip', this.$el).tooltip('fixTitle');
+
+ // We need to add/remove a class to an element that is outside the Vue instance
+ this.addNoCommentClass();
});
- });
- },
- destroyed() {
- $(document).off('toggle.comments');
- },
- watch: {
- storeState: {
- handler() {
- this.$nextTick(() => {
- $('.has-tooltip', this.$el).tooltip('fixTitle');
-
- // We need to add/remove a class to an element that is outside the Vue instance
- this.addNoCommentClass();
- });
- },
- deep: true,
},
+ deep: true,
},
- computed: {
- notesSubset() {
- let notes = [];
-
- if (this.discussion) {
- notes = Object.keys(this.discussion.notes)
- .slice(0, this.shownAvatars)
- .map(noteId => this.discussion.notes[noteId]);
- }
-
- return notes;
- },
- extraNotesTitle() {
- if (this.discussion) {
- const extra = this.discussion.notesCount() - this.shownAvatars;
+ },
+ computed: {
+ notesSubset() {
+ let notes = [];
+
+ if (this.discussion) {
+ notes = Object.keys(this.discussion.notes)
+ .slice(0, this.shownAvatars)
+ .map(noteId => this.discussion.notes[noteId]);
+ }
+
+ return notes;
+ },
+ extraNotesTitle() {
+ if (this.discussion) {
+ const extra = this.discussion.notesCount() - this.shownAvatars;
- return `${extra} more comment${extra > 1 ? 's' : ''}`;
- }
+ return `${extra} more comment${extra > 1 ? 's' : ''}`;
+ }
- return '';
- },
- discussion() {
- return this.storeState[this.discussionId];
- },
- notesCount() {
- if (this.discussion) {
- return this.discussion.notesCount();
- }
+ return '';
+ },
+ discussion() {
+ return this.storeState[this.discussionId];
+ },
+ notesCount() {
+ if (this.discussion) {
+ return this.discussion.notesCount();
+ }
- return 0;
- },
- moreText() {
- const plusSign = this.notesCount < 100 ? '+' : '';
+ return 0;
+ },
+ moreText() {
+ const plusSign = this.notesCount < 100 ? '+' : '';
- return `${plusSign}${this.notesCount - this.shownAvatars}`;
- },
+ return `${plusSign}${this.notesCount - this.shownAvatars}`;
},
- methods: {
- clickedAvatar(e) {
- notes.addDiffNote(e);
+ },
+ methods: {
+ clickedAvatar(e) {
+ notes.addDiffNote(e);
- // Toggle the active state of the toggle all button
- this.toggleDiscussionsToggleState();
+ // Toggle the active state of the toggle all button
+ this.toggleDiscussionsToggleState();
- this.$nextTick(() => {
- this.setDiscussionVisible();
+ this.$nextTick(() => {
+ this.setDiscussionVisible();
- $('.has-tooltip', this.$el).tooltip('fixTitle');
- $('.has-tooltip', this.$el).tooltip('hide');
- });
- },
- addNoCommentClass() {
- const notesCount = this.notesCount;
+ $('.has-tooltip', this.$el).tooltip('fixTitle');
+ $('.has-tooltip', this.$el).tooltip('hide');
+ });
+ },
+ addNoCommentClass() {
+ const notesCount = this.notesCount;
- $(this.$el).closest('.js-avatar-container')
- .toggleClass('js-no-comment-btn', notesCount > 0)
- .nextUntil('.js-avatar-container')
- .toggleClass('js-no-comment-btn', notesCount > 0);
- },
- toggleDiscussionsToggleState() {
- const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
- const $visibleNotesHolders = $notesHolders.filter(':visible');
- const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
+ $(this.$el).closest('.js-avatar-container')
+ .toggleClass('js-no-comment-btn', notesCount > 0)
+ .nextUntil('.js-avatar-container')
+ .toggleClass('js-no-comment-btn', notesCount > 0);
+ },
+ toggleDiscussionsToggleState() {
+ const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
+ const $visibleNotesHolders = $notesHolders.filter(':visible');
+ const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
- $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
- },
- setDiscussionVisible() {
- this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
- },
+ $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
+ },
+ setDiscussionVisible() {
+ this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
},
- });
+ },
+});
- Vue.component('diff-note-avatars', DiffNoteAvatars);
-})();
+Vue.component('diff-note-avatars', DiffNoteAvatars);
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 8edc45130fc..8a0fd3bb4a7 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -4,192 +4,190 @@
import Vue from 'vue';
-(() => {
- const JumpToDiscussion = Vue.extend({
- mixins: [DiscussionMixins],
- props: {
- discussionId: String
+const JumpToDiscussion = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ discussionId: String
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ discussion: {},
+ };
+ },
+ computed: {
+ allResolved: function () {
+ return this.unresolvedDiscussionCount === 0;
},
- data: function () {
- return {
- discussions: CommentsStore.state,
- discussion: {},
- };
- },
- computed: {
- allResolved: function () {
- return this.unresolvedDiscussionCount === 0;
- },
- showButton: function () {
- if (this.discussionId) {
- if (this.unresolvedDiscussionCount > 1) {
- return true;
- } else {
- return this.discussionId !== this.lastResolvedId;
- }
+ showButton: function () {
+ if (this.discussionId) {
+ if (this.unresolvedDiscussionCount > 1) {
+ return true;
} else {
- return this.unresolvedDiscussionCount >= 1;
+ return this.discussionId !== this.lastResolvedId;
}
- },
- lastResolvedId: function () {
- let lastId;
- for (const discussionId in this.discussions) {
- const discussion = this.discussions[discussionId];
-
- if (!discussion.isResolved()) {
- lastId = discussion.id;
- }
- }
- return lastId;
+ } else {
+ return this.unresolvedDiscussionCount >= 1;
}
},
- methods: {
- jumpToNextUnresolvedDiscussion: function () {
- let discussionsSelector;
- let discussionIdsInScope;
- let firstUnresolvedDiscussionId;
- let nextUnresolvedDiscussionId;
- let activeTab = window.mrTabs.currentAction;
- let hasDiscussionsToJumpTo = true;
- let jumpToFirstDiscussion = !this.discussionId;
-
- const discussionIdsForElements = function(elements) {
- return elements.map(function() {
- return $(this).attr('data-discussion-id');
- }).toArray();
- };
-
- const discussions = this.discussions;
-
- if (activeTab === 'diffs') {
- discussionsSelector = '.diffs .notes[data-discussion-id]';
- discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
-
- let unresolvedDiscussionCount = 0;
-
- for (let i = 0; i < discussionIdsInScope.length; i += 1) {
- const discussionId = discussionIdsInScope[i];
- const discussion = discussions[discussionId];
- if (discussion && !discussion.isResolved()) {
- unresolvedDiscussionCount += 1;
- }
- }
+ lastResolvedId: function () {
+ let lastId;
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
- if (this.discussionId && !this.discussion.isResolved()) {
- // If this is the last unresolved discussion on the diffs tab,
- // there are no discussions to jump to.
- if (unresolvedDiscussionCount === 1) {
- hasDiscussionsToJumpTo = false;
- }
- } else {
- // If there are no unresolved discussions on the diffs tab at all,
- // there are no discussions to jump to.
- if (unresolvedDiscussionCount === 0) {
- hasDiscussionsToJumpTo = false;
- }
- }
- } else if (activeTab !== 'notes') {
- // If we are on the commits or builds tabs,
- // there are no discussions to jump to.
- hasDiscussionsToJumpTo = false;
+ if (!discussion.isResolved()) {
+ lastId = discussion.id;
}
+ }
+ return lastId;
+ }
+ },
+ methods: {
+ jumpToNextUnresolvedDiscussion: function () {
+ let discussionsSelector;
+ let discussionIdsInScope;
+ let firstUnresolvedDiscussionId;
+ let nextUnresolvedDiscussionId;
+ let activeTab = window.mrTabs.currentAction;
+ let hasDiscussionsToJumpTo = true;
+ let jumpToFirstDiscussion = !this.discussionId;
+
+ const discussionIdsForElements = function(elements) {
+ return elements.map(function() {
+ return $(this).attr('data-discussion-id');
+ }).toArray();
+ };
- if (!hasDiscussionsToJumpTo) {
- // If there are no discussions to jump to on the current page,
- // switch to the notes tab and jump to the first disucssion there.
- window.mrTabs.activateTab('notes');
- activeTab = 'notes';
- jumpToFirstDiscussion = true;
- }
+ const discussions = this.discussions;
- if (activeTab === 'notes') {
- discussionsSelector = '.discussion[data-discussion-id]';
- discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
- }
+ if (activeTab === 'diffs') {
+ discussionsSelector = '.diffs .notes[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+
+ let unresolvedDiscussionCount = 0;
- let currentDiscussionFound = false;
for (let i = 0; i < discussionIdsInScope.length; i += 1) {
const discussionId = discussionIdsInScope[i];
const discussion = discussions[discussionId];
+ if (discussion && !discussion.isResolved()) {
+ unresolvedDiscussionCount += 1;
+ }
+ }
- if (!discussion) {
- // Discussions for comments on commits in this MR don't have a resolved status.
- continue;
+ if (this.discussionId && !this.discussion.isResolved()) {
+ // If this is the last unresolved discussion on the diffs tab,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 1) {
+ hasDiscussionsToJumpTo = false;
+ }
+ } else {
+ // If there are no unresolved discussions on the diffs tab at all,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 0) {
+ hasDiscussionsToJumpTo = false;
}
+ }
+ } else if (activeTab !== 'notes') {
+ // If we are on the commits or builds tabs,
+ // there are no discussions to jump to.
+ hasDiscussionsToJumpTo = false;
+ }
- if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
- firstUnresolvedDiscussionId = discussionId;
+ if (!hasDiscussionsToJumpTo) {
+ // If there are no discussions to jump to on the current page,
+ // switch to the notes tab and jump to the first disucssion there.
+ window.mrTabs.activateTab('notes');
+ activeTab = 'notes';
+ jumpToFirstDiscussion = true;
+ }
- if (jumpToFirstDiscussion) {
- break;
- }
+ if (activeTab === 'notes') {
+ discussionsSelector = '.discussion[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+ }
+
+ let currentDiscussionFound = false;
+ for (let i = 0; i < discussionIdsInScope.length; i += 1) {
+ const discussionId = discussionIdsInScope[i];
+ const discussion = discussions[discussionId];
+
+ if (!discussion) {
+ // Discussions for comments on commits in this MR don't have a resolved status.
+ continue;
+ }
+
+ if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
+ firstUnresolvedDiscussionId = discussionId;
+
+ if (jumpToFirstDiscussion) {
+ break;
}
+ }
- if (!jumpToFirstDiscussion) {
- if (currentDiscussionFound) {
- if (!discussion.isResolved()) {
- nextUnresolvedDiscussionId = discussionId;
- break;
- }
- else {
- continue;
- }
+ if (!jumpToFirstDiscussion) {
+ if (currentDiscussionFound) {
+ if (!discussion.isResolved()) {
+ nextUnresolvedDiscussionId = discussionId;
+ break;
}
-
- if (discussionId === this.discussionId) {
- currentDiscussionFound = true;
+ else {
+ continue;
}
}
+
+ if (discussionId === this.discussionId) {
+ currentDiscussionFound = true;
+ }
}
+ }
- nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
+ nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
- if (!nextUnresolvedDiscussionId) {
- return;
- }
+ if (!nextUnresolvedDiscussionId) {
+ return;
+ }
- let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
+ let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
- if (activeTab === 'notes') {
- $target = $target.closest('.note-discussion');
+ if (activeTab === 'notes') {
+ $target = $target.closest('.note-discussion');
- // If the next discussion is closed, toggle it open.
- if ($target.find('.js-toggle-content').is(':hidden')) {
- $target.find('.js-toggle-button i').trigger('click');
+ // If the next discussion is closed, toggle it open.
+ if ($target.find('.js-toggle-content').is(':hidden')) {
+ $target.find('.js-toggle-button i').trigger('click');
+ }
+ } else if (activeTab === 'diffs') {
+ // Resolved discussions are hidden in the diffs tab by default.
+ // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
+ // When jumping between unresolved discussions on the diffs tab, we show them.
+ $target.closest(".content").show();
+
+ $target = $target.closest("tr.notes_holder");
+ $target.show();
+
+ // If we are on the diffs tab, we don't scroll to the discussion itself, but to
+ // 4 diff lines above it: the line the discussion was in response to + 3 context
+ let prevEl;
+ for (let i = 0; i < 4; i += 1) {
+ prevEl = $target.prev();
+
+ // If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
+ if (!prevEl.hasClass("line_holder")) {
+ break;
}
- } else if (activeTab === 'diffs') {
- // Resolved discussions are hidden in the diffs tab by default.
- // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
- // When jumping between unresolved discussions on the diffs tab, we show them.
- $target.closest(".content").show();
-
- $target = $target.closest("tr.notes_holder");
- $target.show();
-
- // If we are on the diffs tab, we don't scroll to the discussion itself, but to
- // 4 diff lines above it: the line the discussion was in response to + 3 context
- let prevEl;
- for (let i = 0; i < 4; i += 1) {
- prevEl = $target.prev();
-
- // If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
- if (!prevEl.hasClass("line_holder")) {
- break;
- }
- $target = prevEl;
- }
+ $target = prevEl;
}
-
- $.scrollTo($target, {
- offset: 0
- });
}
- },
- created() {
- this.discussion = this.discussions[this.discussionId];
- },
- });
- Vue.component('jump-to-discussion', JumpToDiscussion);
-})();
+ $.scrollTo($target, {
+ offset: 0
+ });
+ }
+ },
+ created() {
+ this.discussion = this.discussions[this.discussionId];
+ },
+});
+
+Vue.component('jump-to-discussion', JumpToDiscussion);
diff --git a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
index 8eb0e10b832..e0c09aa0eee 100644
--- a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
@@ -2,29 +2,27 @@
import Vue from 'vue';
-(() => {
- const NewIssueForDiscussion = Vue.extend({
- props: {
- discussionId: {
- type: String,
- required: true,
- },
+const NewIssueForDiscussion = Vue.extend({
+ props: {
+ discussionId: {
+ type: String,
+ required: true,
},
- data() {
- return {
- discussions: CommentsStore.state,
- };
+ },
+ data() {
+ return {
+ discussions: CommentsStore.state,
+ };
+ },
+ computed: {
+ discussion() {
+ return this.discussions[this.discussionId];
},
- computed: {
- discussion() {
- return this.discussions[this.discussionId];
- },
- showButton() {
- if (this.discussion) return !this.discussion.isResolved();
- return false;
- },
+ showButton() {
+ if (this.discussion) return !this.discussion.isResolved();
+ return false;
},
- });
+ },
+});
- Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
-})();
+Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index 312f38ce241..92f6fd654b3 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -5,117 +5,119 @@
import Vue from 'vue';
-(() => {
- const ResolveBtn = Vue.extend({
- props: {
- noteId: Number,
- discussionId: String,
- resolved: Boolean,
- canResolve: Boolean,
- resolvedBy: String,
- authorName: String,
- authorAvatar: String,
- noteTruncated: String,
+const ResolveBtn = Vue.extend({
+ props: {
+ noteId: Number,
+ discussionId: String,
+ resolved: Boolean,
+ canResolve: Boolean,
+ resolvedBy: String,
+ authorName: String,
+ authorAvatar: String,
+ noteTruncated: String,
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ loading: false
+ };
+ },
+ watch: {
+ 'discussions': {
+ handler: 'updateTooltip',
+ deep: true
+ }
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
},
- data: function () {
- return {
- discussions: CommentsStore.state,
- loading: false,
- note: {},
- };
+ note: function () {
+ return this.discussion ? this.discussion.getNote(this.noteId) : {};
},
- watch: {
- 'discussions': {
- handler: 'updateTooltip',
- deep: true
+ buttonText: function () {
+ if (this.isResolved) {
+ return `Resolved by ${this.resolvedByName}`;
+ } else if (this.canResolve) {
+ return 'Mark as resolved';
+ } else {
+ return 'Unable to resolve';
}
},
- computed: {
- discussion: function () {
- return this.discussions[this.discussionId];
- },
- buttonText: function () {
- if (this.isResolved) {
- return `Resolved by ${this.resolvedByName}`;
- } else if (this.canResolve) {
- return 'Mark as resolved';
- } else {
- return 'Unable to resolve';
- }
- },
- isResolved: function () {
- if (this.note) {
- return this.note.resolved;
- } else {
- return false;
- }
- },
- resolvedByName: function () {
- return this.note.resolved_by;
- },
+ isResolved: function () {
+ if (this.note) {
+ return this.note.resolved;
+ } else {
+ return false;
+ }
},
- methods: {
- updateTooltip: function () {
- this.$nextTick(() => {
- $(this.$refs.button)
- .tooltip('hide')
- .tooltip('fixTitle');
- });
- },
- resolve: function () {
- if (!this.canResolve) return;
+ resolvedByName: function () {
+ return this.note.resolved_by;
+ },
+ },
+ methods: {
+ updateTooltip: function () {
+ this.$nextTick(() => {
+ $(this.$refs.button)
+ .tooltip('hide')
+ .tooltip('fixTitle');
+ });
+ },
+ resolve: function () {
+ const errorFlashMsg = 'An error occurred when trying to resolve a comment. Please try again.';
- let promise;
- this.loading = true;
+ if (!this.canResolve) return;
- if (this.isResolved) {
- promise = ResolveService
- .unresolve(this.noteId);
- } else {
- promise = ResolveService
- .resolve(this.noteId);
- }
+ let promise;
+ this.loading = true;
- promise.then((response) => {
- this.loading = false;
+ if (this.isResolved) {
+ promise = ResolveService
+ .unresolve(this.noteId);
+ } else {
+ promise = ResolveService
+ .resolve(this.noteId);
+ }
- if (response.status === 200) {
- const data = response.json();
- const resolved_by = data ? data.resolved_by : null;
+ promise.then((response) => {
+ this.loading = false;
- CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
- this.discussion.updateHeadline(data);
- } else {
- new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
- }
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
- this.updateTooltip();
- });
- }
- },
- mounted: function () {
- $(this.$refs.button).tooltip({
- container: 'body'
- });
- },
- beforeDestroy: function () {
- CommentsStore.delete(this.discussionId, this.noteId);
- },
- created: function () {
- CommentsStore.create({
- discussionId: this.discussionId,
- noteId: this.noteId,
- canResolve: this.canResolve,
- resolved: this.resolved,
- resolvedBy: this.resolvedBy,
- authorName: this.authorName,
- authorAvatar: this.authorAvatar,
- noteTruncated: this.noteTruncated,
- });
+ CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
+ this.discussion.updateHeadline(data);
+ } else {
+ new Flash(errorFlashMsg);
+ }
- this.note = this.discussion.getNote(this.noteId);
+ this.updateTooltip();
+ }).catch(() => {
+ new Flash(errorFlashMsg);
+ });
}
- });
+ },
+ mounted: function () {
+ $(this.$refs.button).tooltip({
+ container: 'body'
+ });
+ },
+ beforeDestroy: function () {
+ CommentsStore.delete(this.discussionId, this.noteId);
+ },
+ created: function () {
+ CommentsStore.create({
+ discussionId: this.discussionId,
+ noteId: this.noteId,
+ canResolve: this.canResolve,
+ resolved: this.resolved,
+ resolvedBy: this.resolvedBy,
+ authorName: this.authorName,
+ authorAvatar: this.authorAvatar,
+ noteTruncated: this.noteTruncated,
+ });
+ }
+});
- Vue.component('resolve-btn', ResolveBtn);
-})();
+Vue.component('resolve-btn', ResolveBtn);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
index 27147ac6b5c..96e5a440357 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
@@ -4,24 +4,22 @@
import Vue from 'vue';
-((w) => {
- w.ResolveCount = Vue.extend({
- mixins: [DiscussionMixins],
- props: {
- loggedOut: Boolean
+window.ResolveCount = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ loggedOut: Boolean
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state
+ };
+ },
+ computed: {
+ allResolved: function () {
+ return this.resolvedDiscussionCount === this.discussionCount;
},
- data: function () {
- return {
- discussions: CommentsStore.state
- };
- },
- computed: {
- allResolved: function () {
- return this.resolvedDiscussionCount === this.discussionCount;
- },
- resolvedCountText() {
- return this.discussionCount === 1 ? 'discussion' : 'discussions';
- }
+ resolvedCountText() {
+ return this.discussionCount === 1 ? 'discussion' : 'discussions';
}
- });
-})(window);
+ }
+});
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
index a964b7d0c6b..6a036e96171 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
@@ -4,59 +4,57 @@
import Vue from 'vue';
-(() => {
- const ResolveDiscussionBtn = Vue.extend({
- props: {
- discussionId: String,
- mergeRequestId: Number,
- canResolve: Boolean,
- },
- data: function() {
- return {
- discussion: {},
- };
+const ResolveDiscussionBtn = Vue.extend({
+ props: {
+ discussionId: String,
+ mergeRequestId: Number,
+ canResolve: Boolean,
+ },
+ data: function() {
+ return {
+ discussion: {},
+ };
+ },
+ computed: {
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
},
- computed: {
- showButton: function () {
- if (this.discussion) {
- return this.discussion.isResolvable();
- } else {
- return false;
- }
- },
- isDiscussionResolved: function () {
- if (this.discussion) {
- return this.discussion.isResolved();
- } else {
- return false;
- }
- },
- buttonText: function () {
- if (this.isDiscussionResolved) {
- return "Unresolve discussion";
- } else {
- return "Resolve discussion";
- }
- },
- loading: function () {
- if (this.discussion) {
- return this.discussion.loading;
- } else {
- return false;
- }
+ isDiscussionResolved: function () {
+ if (this.discussion) {
+ return this.discussion.isResolved();
+ } else {
+ return false;
}
},
- methods: {
- resolve: function () {
- ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ return "Unresolve discussion";
+ } else {
+ return "Resolve discussion";
}
},
- created: function () {
- CommentsStore.createDiscussion(this.discussionId, this.canResolve);
-
- this.discussion = CommentsStore.state[this.discussionId];
+ loading: function () {
+ if (this.discussion) {
+ return this.discussion.loading;
+ } else {
+ return false;
+ }
+ }
+ },
+ methods: {
+ resolve: function () {
+ ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
}
- });
+ },
+ created: function () {
+ CommentsStore.createDiscussion(this.discussionId, this.canResolve);
+
+ this.discussion = CommentsStore.state[this.discussionId];
+ }
+});
- Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
-})();
+Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js b/app/assets/javascripts/diff_notes/mixins/discussion.js
index 3c08c222f46..36c4abf02cf 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js
@@ -1,37 +1,35 @@
/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */
-((w) => {
- w.DiscussionMixins = {
- computed: {
- discussionCount: function () {
- return Object.keys(this.discussions).length;
- },
- resolvedDiscussionCount: function () {
- let resolvedCount = 0;
+window.DiscussionMixins = {
+ computed: {
+ discussionCount: function () {
+ return Object.keys(this.discussions).length;
+ },
+ resolvedDiscussionCount: function () {
+ let resolvedCount = 0;
- for (const discussionId in this.discussions) {
- const discussion = this.discussions[discussionId];
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
- if (discussion.isResolved()) {
- resolvedCount += 1;
- }
+ if (discussion.isResolved()) {
+ resolvedCount += 1;
}
+ }
- return resolvedCount;
- },
- unresolvedDiscussionCount: function () {
- let unresolvedCount = 0;
+ return resolvedCount;
+ },
+ unresolvedDiscussionCount: function () {
+ let unresolvedCount = 0;
- for (const discussionId in this.discussions) {
- const discussion = this.discussions[discussionId];
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
- if (!discussion.isResolved()) {
- unresolvedCount += 1;
- }
+ if (!discussion.isResolved()) {
+ unresolvedCount += 1;
}
-
- return unresolvedCount;
}
+
+ return unresolvedCount;
}
- };
-})(window);
+ }
+};
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index bfa4fc9037a..4ea6ba8a73d 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -9,76 +9,76 @@ require('../../vue_shared/vue_resource_interceptor');
Vue.use(VueResource);
-(() => {
- window.gl = window.gl || {};
+window.gl = window.gl || {};
- class ResolveServiceClass {
- constructor(root) {
- this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
- this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
- }
-
- resolve(noteId) {
- return this.noteResource.save({ noteId }, {});
- }
+class ResolveServiceClass {
+ constructor(root) {
+ this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
+ this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
+ }
- unresolve(noteId) {
- return this.noteResource.delete({ noteId }, {});
- }
+ resolve(noteId) {
+ return this.noteResource.save({ noteId }, {});
+ }
- toggleResolveForDiscussion(mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
- const isResolved = discussion.isResolved();
- let promise;
+ unresolve(noteId) {
+ return this.noteResource.delete({ noteId }, {});
+ }
- if (isResolved) {
- promise = this.unResolveAll(mergeRequestId, discussionId);
- } else {
- promise = this.resolveAll(mergeRequestId, discussionId);
- }
+ toggleResolveForDiscussion(mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
+ const isResolved = discussion.isResolved();
+ let promise;
- promise.then((response) => {
- discussion.loading = false;
+ if (isResolved) {
+ promise = this.unResolveAll(mergeRequestId, discussionId);
+ } else {
+ promise = this.resolveAll(mergeRequestId, discussionId);
+ }
- if (response.status === 200) {
- const data = response.json();
- const resolved_by = data ? data.resolved_by : null;
+ promise.then((response) => {
+ discussion.loading = false;
- if (isResolved) {
- discussion.unResolveAllNotes();
- } else {
- discussion.resolveAllNotes(resolved_by);
- }
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
- discussion.updateHeadline(data);
+ if (isResolved) {
+ discussion.unResolveAllNotes();
} else {
- new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
+ discussion.resolveAllNotes(resolved_by);
}
- });
- }
- resolveAll(mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
+ discussion.updateHeadline(data);
+ } else {
+ throw new Error('An error occurred when trying to resolve discussion.');
+ }
+ }).catch(() => {
+ new Flash('An error occurred when trying to resolve a discussion. Please try again.');
+ });
+ }
- discussion.loading = true;
+ resolveAll(mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
- return this.discussionResource.save({
- mergeRequestId,
- discussionId
- }, {});
- }
+ discussion.loading = true;
+
+ return this.discussionResource.save({
+ mergeRequestId,
+ discussionId
+ }, {});
+ }
- unResolveAll(mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
+ unResolveAll(mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
- discussion.loading = true;
+ discussion.loading = true;
- return this.discussionResource.delete({
- mergeRequestId,
- discussionId
- }, {});
- }
+ return this.discussionResource.delete({
+ mergeRequestId,
+ discussionId
+ }, {});
}
+}
- gl.DiffNotesResolveServiceClass = ResolveServiceClass;
-})();
+gl.DiffNotesResolveServiceClass = ResolveServiceClass;
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js
index e6cbda56c91..d802db7d3af 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js
+++ b/app/assets/javascripts/diff_notes/stores/comments.js
@@ -3,56 +3,54 @@
import Vue from 'vue';
-((w) => {
- w.CommentsStore = {
- state: {},
- get: function (discussionId, noteId) {
- return this.state[discussionId].getNote(noteId);
- },
- createDiscussion: function (discussionId, canResolve) {
- let discussion = this.state[discussionId];
- if (!this.state[discussionId]) {
- discussion = new DiscussionModel(discussionId);
- Vue.set(this.state, discussionId, discussion);
- }
+window.CommentsStore = {
+ state: {},
+ get: function (discussionId, noteId) {
+ return this.state[discussionId].getNote(noteId);
+ },
+ createDiscussion: function (discussionId, canResolve) {
+ let discussion = this.state[discussionId];
+ if (!this.state[discussionId]) {
+ discussion = new DiscussionModel(discussionId);
+ Vue.set(this.state, discussionId, discussion);
+ }
- if (canResolve !== undefined) {
- discussion.canResolve = canResolve;
- }
+ if (canResolve !== undefined) {
+ discussion.canResolve = canResolve;
+ }
- return discussion;
- },
- create: function (noteObj) {
- const discussion = this.createDiscussion(noteObj.discussionId);
+ return discussion;
+ },
+ create: function (noteObj) {
+ const discussion = this.createDiscussion(noteObj.discussionId);
+
+ discussion.createNote(noteObj);
+ },
+ update: function (discussionId, noteId, resolved, resolved_by) {
+ const discussion = this.state[discussionId];
+ const note = discussion.getNote(noteId);
+ note.resolved = resolved;
+ note.resolved_by = resolved_by;
+ },
+ delete: function (discussionId, noteId) {
+ const discussion = this.state[discussionId];
+ discussion.deleteNote(noteId);
+
+ if (discussion.notesCount() === 0) {
+ Vue.delete(this.state, discussionId);
+ }
+ },
+ unresolvedDiscussionIds: function () {
+ const ids = [];
- discussion.createNote(noteObj);
- },
- update: function (discussionId, noteId, resolved, resolved_by) {
- const discussion = this.state[discussionId];
- const note = discussion.getNote(noteId);
- note.resolved = resolved;
- note.resolved_by = resolved_by;
- },
- delete: function (discussionId, noteId) {
+ for (const discussionId in this.state) {
const discussion = this.state[discussionId];
- discussion.deleteNote(noteId);
- if (discussion.notesCount() === 0) {
- Vue.delete(this.state, discussionId);
+ if (!discussion.isResolved()) {
+ ids.push(discussion.id);
}
- },
- unresolvedDiscussionIds: function () {
- const ids = [];
-
- for (const discussionId in this.state) {
- const discussion = this.state[discussionId];
-
- if (!discussion.isResolved()) {
- ids.push(discussion.id);
- }
- }
-
- return ids;
}
- };
-})(window);
+
+ return ids;
+ }
+};
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index f277e1dddc7..20db2698ba8 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -33,6 +33,7 @@
/* global Labels */
/* global Shortcuts */
/* global Sidebar */
+/* global ShortcutsWiki */
import Issue from './issue';
@@ -46,6 +47,7 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
+import ShortcutsWiki from './shortcuts_wiki';
const ShortcutsBlob = require('./shortcuts_blob');
@@ -89,11 +91,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
fileBlobPermalinkUrl,
});
- new BlobForkSuggestion(
- document.querySelector('.js-edit-blob-link-fork-toggler'),
- document.querySelector('.js-cancel-fork-suggestion'),
- document.querySelector('.js-file-fork-suggestion-section'),
- );
+ new BlobForkSuggestion({
+ openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'),
+ forkButtons: document.querySelectorAll('.js-fork-suggestion-button'),
+ cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
+ suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'),
+ actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'),
+ });
}
switch (page) {
@@ -148,13 +152,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:milestones:new':
case 'projects:milestones:edit':
case 'projects:milestones:update':
+ case 'groups:milestones:new':
+ case 'groups:milestones:edit':
+ case 'groups:milestones:update':
new ZenMode();
new gl.DueDateSelectors();
new gl.GLForm($('.milestone-form'));
break;
- case 'groups:milestones:new':
- new ZenMode();
- break;
case 'projects:compare:show':
new gl.Diff();
break;
@@ -365,6 +369,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'admin':
new Admin();
switch (path[1]) {
+ case 'cohorts':
+ new gl.UsagePing();
+ break;
case 'groups':
new UsersSelect();
break;
@@ -416,7 +423,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
break;
case 'wikis':
new gl.Wikis();
- shortcut_handler = new ShortcutsNavigation();
+ shortcut_handler = new ShortcutsWiki();
new ZenMode();
new gl.GLForm($('.wiki-form'));
break;
diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js
index a23d914772a..8883ed9aa14 100644
--- a/app/assets/javascripts/droplab/constants.js
+++ b/app/assets/javascripts/droplab/constants.js
@@ -2,10 +2,12 @@ const DATA_TRIGGER = 'data-dropdown-trigger';
const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active';
+const IGNORE_CLASS = 'droplab-item-ignore';
export {
DATA_TRIGGER,
DATA_DROPDOWN,
SELECTED_CLASS,
ACTIVE_CLASS,
+ IGNORE_CLASS,
};
diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js
index 9588921ebcd..1fb4d63923c 100644
--- a/app/assets/javascripts/droplab/drop_down.js
+++ b/app/assets/javascripts/droplab/drop_down.js
@@ -1,7 +1,7 @@
/* eslint-disable */
import utils from './utils';
-import { SELECTED_CLASS } from './constants';
+import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
var DropDown = function(list) {
this.currentIndex = 0;
@@ -36,6 +36,7 @@ Object.assign(DropDown.prototype, {
clickEvent: function(e) {
if (e.target.tagName === 'UL') return;
+ if (e.target.classList.contains(IGNORE_CLASS)) return;
var selected = utils.closest(e.target, 'LI');
if (!selected) return;
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index f2963a5eb19..b70d242269d 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -38,6 +38,9 @@ window.DropzoneInput = (function() {
"opacity": 0,
"display": "none"
});
+
+ if (!project_uploads_path) return;
+
dropzone = form_dropzone.dropzone({
url: project_uploads_path,
dictDefaultMessage: "",
@@ -66,7 +69,10 @@ window.DropzoneInput = (function() {
form_textarea.focus();
},
success: function(header, response) {
- pasteText(response.link.markdown);
+ const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length;
+ const shouldPad = processingFileCount >= 1;
+
+ pasteText(response.link.markdown, shouldPad);
},
error: function(temp) {
var checkIfMsgExists, errorAlert;
@@ -123,16 +129,19 @@ window.DropzoneInput = (function() {
}
return false;
};
- pasteText = function(text) {
+ pasteText = function(text, shouldPad) {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
- var formattedText = text + "\n\n";
- caretStart = $(child)[0].selectionStart;
- caretEnd = $(child)[0].selectionEnd;
+ var formattedText = text;
+ if (shouldPad) formattedText += "\n\n";
+ const textarea = child.get(0);
+ caretStart = textarea.selectionStart;
+ caretEnd = textarea.selectionEnd;
textEnd = $(child).val().length;
beforeSelection = $(child).val().substring(0, caretStart);
afterSelection = $(child).val().substring(caretEnd, textEnd);
$(child).val(beforeSelection + formattedText + afterSelection);
- child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
+ textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
+ textarea.style.height = `${textarea.scrollHeight}px`;
return form_textarea.trigger("input");
};
getFilename = function(e) {
@@ -176,7 +185,7 @@ window.DropzoneInput = (function() {
};
insertToTextArea = function(filename, url) {
return $(child).val(function(index, val) {
- return val.replace("{{" + filename + "}}", url + "\n");
+ return val.replace("{{" + filename + "}}", url);
});
};
appendToTextArea = function(url) {
@@ -211,6 +220,7 @@ window.DropzoneInput = (function() {
form.find(".markdown-selector").click(function(e) {
e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click();
+ form_textarea.focus();
});
}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index db10b383913..a8fc5b41fb4 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -115,11 +115,13 @@ class DueDateSelect {
this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide();
this.$value.css('display', '');
+ const fadeOutLoader = () => {
+ this.$loading.fadeOut();
+ };
gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
- .then(() => {
- this.$loading.fadeOut();
- });
+ .then(fadeOutLoader)
+ .catch(fadeOutLoader);
}
submitSelectedDate(isDropdown) {
@@ -168,8 +170,9 @@ class DueDateSelectors {
const $datePicker = $(this);
const calendar = new Pikaday({
field: $datePicker.get(0),
- theme: 'gitlab-theme',
+ theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
+ container: $datePicker.parent().get(0),
onSelect(dateText) {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js
index 0518422e475..f7175e412da 100644
--- a/app/assets/javascripts/environments/components/environment.js
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -2,7 +2,7 @@
/* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
-import EnvironmentTable from './environments_table';
+import EnvironmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination';
import '../../lib/utils/common_utils';
diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js
deleted file mode 100644
index 1418e8d86ee..00000000000
--- a/app/assets/javascripts/environments/components/environment_actions.js
+++ /dev/null
@@ -1,97 +0,0 @@
-/* global Flash */
-/* eslint-disable no-new */
-
-import playIconSvg from 'icons/_icon_play.svg';
-import eventHub from '../event_hub';
-
-export default {
- props: {
- actions: {
- type: Array,
- required: false,
- default: () => [],
- },
-
- service: {
- type: Object,
- required: true,
- },
- },
-
- data() {
- return {
- playIconSvg,
- isLoading: false,
- };
- },
-
- computed: {
- title() {
- return 'Deploy to...';
- },
- },
-
- methods: {
- onClickAction(endpoint) {
- this.isLoading = true;
-
- this.service.postAction(endpoint)
- .then(() => {
- this.isLoading = false;
- eventHub.$emit('refreshEnvironments');
- })
- .catch(() => {
- this.isLoading = false;
- new Flash('An error occured while making the request.');
- });
- },
-
- isActionDisabled(action) {
- if (action.playable === undefined) {
- return false;
- }
-
- return !action.playable;
- },
- },
-
- template: `
- <div class="btn-group" role="group">
- <button
- type="button"
- class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
- data-container="body"
- data-toggle="dropdown"
- :title="title"
- :aria-label="title"
- :disabled="isLoading">
- <span>
- <span v-html="playIconSvg"></span>
- <i
- class="fa fa-caret-down"
- aria-hidden="true"/>
- <i
- v-if="isLoading"
- class="fa fa-spinner fa-spin"
- aria-hidden="true"/>
- </span>
- </button>
-
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for="action in actions">
- <button
- type="button"
- class="js-manual-action-link no-btn btn"
- @click="onClickAction(action.play_path)"
- :class="{ 'disabled': isActionDisabled(action) }"
- :disabled="isActionDisabled(action)">
- ${playIconSvg}
- <span>
- {{action.name}}
- </span>
- </button>
- </li>
- </ul>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
new file mode 100644
index 00000000000..e81c97260d7
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -0,0 +1,103 @@
+<script>
+/* global Flash */
+/* eslint-disable no-new */
+
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ playIconSvg,
+ isLoading: false,
+ };
+ },
+
+ computed: {
+ title() {
+ return 'Deploy to...';
+ },
+ },
+
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
+
+ $(this.$refs.tooltip).tooltip('destroy');
+
+ this.service.postAction(endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshEnvironments');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+
+ isActionDisabled(action) {
+ if (action.playable === undefined) {
+ return false;
+ }
+
+ return !action.playable;
+ },
+ },
+};
+</script>
+<template>
+ <div
+ class="btn-group"
+ role="group">
+ <button
+ type="button"
+ class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
+ data-container="body"
+ data-toggle="dropdown"
+ ref="tooltip"
+ :title="title"
+ :aria-label="title"
+ :disabled="isLoading">
+ <span>
+ <span v-html="playIconSvg"></span>
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true"/>
+ <i
+ v-if="isLoading"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"/>
+ </span>
+ </button>
+
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <button
+ type="button"
+ class="js-manual-action-link no-btn btn"
+ @click="onClickAction(action.play_path)"
+ :class="{ disabled: isActionDisabled(action) }"
+ :disabled="isActionDisabled(action)">
+ <span v-html="playIconSvg"></span>
+ <span>
+ {{action.name}}
+ </span>
+ </button>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js
deleted file mode 100644
index d79b916c360..00000000000
--- a/app/assets/javascripts/environments/components/environment_external_url.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Renders the external url link in environments table.
- */
-export default {
- props: {
- externalUrl: {
- type: String,
- default: '',
- },
- },
-
- computed: {
- title() {
- return 'Open';
- },
- },
-
- template: `
- <a
- class="btn external-url has-tooltip"
- data-container="body"
- :href="externalUrl"
- target="_blank"
- rel="noopener noreferrer nofollow"
- :title="title"
- :aria-label="title">
- <i class="fa fa-external-link" aria-hidden="true"></i>
- </a>
- `,
-};
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
new file mode 100644
index 00000000000..eaeec2bc53c
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -0,0 +1,33 @@
+<script>
+/**
+ * Renders the external url link in environments table.
+ */
+export default {
+ props: {
+ externalUrl: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ title() {
+ return 'Open';
+ },
+ },
+};
+</script>
+<template>
+ <a
+ class="btn external-url has-tooltip"
+ data-container="body"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ :title="title"
+ :aria-label="title"
+ :href="externalUrl">
+ <i
+ class="fa fa-external-link"
+ aria-hidden="true" />
+ </a>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.vue
index d9b49287dec..73679de6039 100644
--- a/app/assets/javascripts/environments/components/environment_item.js
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,11 +1,12 @@
+<script>
import Timeago from 'timeago.js';
import '../../lib/utils/text_utility';
-import ActionsComponent from './environment_actions';
-import ExternalUrlComponent from './environment_external_url';
-import StopComponent from './environment_stop';
-import RollbackComponent from './environment_rollback';
-import TerminalButtonComponent from './environment_terminal_button';
-import MonitoringButtonComponent from './environment_monitoring';
+import ActionsComponent from './environment_actions.vue';
+import ExternalUrlComponent from './environment_external_url.vue';
+import StopComponent from './environment_stop.vue';
+import RollbackComponent from './environment_rollback.vue';
+import TerminalButtonComponent from './environment_terminal_button.vue';
+import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit';
import eventHub from '../event_hub';
@@ -434,117 +435,140 @@ export default {
eventHub.$emit('toggleFolder', this.model, this.folderUrl);
},
},
-
- template: `
- <tr :class="{ 'js-child-row': model.isChildren }">
- <td>
- <a v-if="!model.isFolder"
- class="environment-name"
- :class="{ 'prepend-left-default': model.isChildren }"
- :href="environmentPath">
- {{model.name}}
- </a>
- <span v-else
- class="folder-name"
- @click="onClickFolder"
- role="button">
-
- <span class="folder-icon">
- <i
- v-show="model.isOpen"
- class="fa fa-caret-down"
- aria-hidden="true" />
- <i
- v-show="!model.isOpen"
- class="fa fa-caret-right"
- aria-hidden="true"/>
- </span>
-
- <span class="folder-icon">
- <i class="fa fa-folder" aria-hidden="true"></i>
- </span>
-
- <span>
- {{model.folderName}}
- </span>
-
- <span class="badge">
- {{model.size}}
- </span>
+};
+</script>
+<template>
+ <tr :class="{ 'js-child-row': model.isChildren }">
+ <td>
+ <a
+ v-if="!model.isFolder"
+ class="environment-name"
+ :class="{ 'prepend-left-default': model.isChildren }"
+ :href="environmentPath">
+ {{model.name}}
+ </a>
+ <span
+ v-else
+ class="folder-name"
+ @click="onClickFolder"
+ role="button">
+
+ <span class="folder-icon">
+ <i
+ v-show="model.isOpen"
+ class="fa fa-caret-down"
+ aria-hidden="true" />
+ <i
+ v-show="!model.isOpen"
+ class="fa fa-caret-right"
+ aria-hidden="true"/>
</span>
- </td>
- <td class="deployment-column">
- <span v-if="shouldRenderDeploymentID">
- {{deploymentInternalId}}
+ <span class="folder-icon">
+ <i
+ class="fa fa-folder"
+ aria-hidden="true" />
</span>
- <span v-if="!model.isFolder && deploymentHasUser">
- by
- <a :href="deploymentUser.web_url" class="js-deploy-user-container">
- <img class="avatar has-tooltip s20"
- :src="deploymentUser.avatar_url"
- :alt="userImageAltDescription"
- :title="deploymentUser.username" />
- </a>
+ <span>
+ {{model.folderName}}
</span>
- </td>
- <td class="environments-build-cell">
- <a v-if="shouldRenderBuildName"
- class="build-link"
- :href="buildPath">
- {{buildName}}
- </a>
- </td>
-
- <td>
- <div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component">
- <commit-component
- :tag="commitTag"
- :commit-ref="commitRef"
- :commit-url="commitUrl"
- :short-sha="commitShortSha"
- :title="commitTitle"
- :author="commitAuthor"/>
- </div>
- <p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title">
- No deployments yet
- </p>
- </td>
-
- <td>
- <span v-if="!model.isFolder && canShowDate"
- class="environment-created-date-timeago">
- {{createdDate}}
+ <span class="badge">
+ {{model.size}}
</span>
- </td>
-
- <td class="environments-actions">
- <div v-if="!model.isFolder" class="btn-group pull-right" role="group">
- <actions-component v-if="hasManualActions && canCreateDeployment"
- :service="service"
- :actions="manualActions"/>
-
- <external-url-component v-if="externalURL && canReadEnvironment"
- :external-url="externalURL"/>
-
- <monitoring-button-component v-if="monitoringUrl && canReadEnvironment"
- :monitoring-url="monitoringUrl"/>
-
- <terminal-button-component v-if="model && model.terminal_path"
- :terminal-path="model.terminal_path"/>
-
- <stop-component v-if="hasStopAction && canCreateDeployment"
- :stop-url="model.stop_path"
- :service="service"/>
-
- <rollback-component v-if="canRetry && canCreateDeployment"
- :is-last-deployment="isLastDeployment"
- :retry-url="retryUrl"
- :service="service"/>
- </div>
- </td>
- </tr>
- `,
-};
+ </span>
+ </td>
+
+ <td class="deployment-column">
+ <span v-if="shouldRenderDeploymentID">
+ {{deploymentInternalId}}
+ </span>
+
+ <span v-if="!model.isFolder && deploymentHasUser">
+ by
+ <a
+ :href="deploymentUser.web_url"
+ class="js-deploy-user-container">
+ <img
+ class="avatar has-tooltip s20"
+ :src="deploymentUser.avatar_url"
+ :alt="userImageAltDescription"
+ :title="deploymentUser.username" />
+ </a>
+ </span>
+ </td>
+
+ <td class="environments-build-cell">
+ <a
+ v-if="shouldRenderBuildName"
+ class="build-link"
+ :href="buildPath">
+ {{buildName}}
+ </a>
+ </td>
+
+ <td>
+ <div
+ v-if="!model.isFolder && hasLastDeploymentKey"
+ class="js-commit-component">
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"/>
+ </div>
+ <p
+ v-if="!model.isFolder && !hasLastDeploymentKey"
+ class="commit-title">
+ No deployments yet
+ </p>
+ </td>
+
+ <td>
+ <span
+ v-if="!model.isFolder && canShowDate"
+ class="environment-created-date-timeago">
+ {{createdDate}}
+ </span>
+ </td>
+
+ <td class="environments-actions">
+ <div
+ v-if="!model.isFolder"
+ class="btn-group pull-right"
+ role="group">
+
+ <actions-component
+ v-if="hasManualActions && canCreateDeployment"
+ :service="service"
+ :actions="manualActions"/>
+
+ <external-url-component
+ v-if="externalURL && canReadEnvironment"
+ :external-url="externalURL"/>
+
+ <monitoring-button-component
+ v-if="monitoringUrl && canReadEnvironment"
+ :monitoring-url="monitoringUrl"/>
+
+ <terminal-button-component
+ v-if="model && model.terminal_path"
+ :terminal-path="model.terminal_path"/>
+
+ <stop-component
+ v-if="hasStopAction && canCreateDeployment"
+ :stop-url="model.stop_path"
+ :service="service"/>
+
+ <rollback-component
+ v-if="canRetry && canCreateDeployment"
+ :is-last-deployment="isLastDeployment"
+ :retry-url="retryUrl"
+ :service="service"/>
+ </div>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.js b/app/assets/javascripts/environments/components/environment_monitoring.js
deleted file mode 100644
index 064e2fc7434..00000000000
--- a/app/assets/javascripts/environments/components/environment_monitoring.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Renders the Monitoring (Metrics) link in environments table.
- */
-export default {
- props: {
- monitoringUrl: {
- type: String,
- default: '',
- required: true,
- },
- },
-
- computed: {
- title() {
- return 'Monitoring';
- },
- },
-
- template: `
- <a
- class="btn monitoring-url has-tooltip"
- data-container="body"
- :href="monitoringUrl"
- target="_blank"
- rel="noopener noreferrer nofollow"
- :title="title"
- :aria-label="title">
- <i class="fa fa-area-chart" aria-hidden="true"></i>
- </a>
- `,
-};
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
new file mode 100644
index 00000000000..4b030a27900
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -0,0 +1,33 @@
+<script>
+/**
+ * Renders the Monitoring (Metrics) link in environments table.
+ */
+export default {
+ props: {
+ monitoringUrl: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ title() {
+ return 'Monitoring';
+ },
+ },
+};
+</script>
+<template>
+ <a
+ class="btn monitoring-url has-tooltip"
+ data-container="body"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ :href="monitoringUrl"
+ :title="title"
+ :aria-label="title">
+ <i
+ class="fa fa-area-chart"
+ aria-hidden="true" />
+ </a>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js b/app/assets/javascripts/environments/components/environment_rollback.vue
index baa15d9e5b5..f139f24036f 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.js
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -1,3 +1,4 @@
+<script>
/* global Flash */
/* eslint-disable no-new */
/**
@@ -36,6 +37,8 @@ export default {
onClick() {
this.isLoading = true;
+ $(this.$el).tooltip('destroy');
+
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
@@ -47,21 +50,25 @@ export default {
});
},
},
+};
+</script>
+<template>
+ <button
+ type="button"
+ class="btn"
+ @click="onClick"
+ :disabled="isLoading">
- template: `
- <button type="button"
- class="btn"
- @click="onClick"
- :disabled="isLoading">
-
- <span v-if="isLastDeployment">
- Re-deploy
- </span>
- <span v-else>
- Rollback
- </span>
+ <span v-if="isLastDeployment">
+ Re-deploy
+ </span>
+ <span v-else>
+ Rollback
+ </span>
- <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
- </button>
- `,
-};
+ <i
+ v-if="isLoading"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ </button>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.vue
index 47102692024..11e9aff7b92 100644
--- a/app/assets/javascripts/environments/components/environment_stop.js
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -1,3 +1,4 @@
+<script>
/* global Flash */
/* eslint-disable no-new, no-alert */
/**
@@ -36,6 +37,8 @@ export default {
if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
+ $(this.$el).tooltip('destroy');
+
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
@@ -48,17 +51,23 @@ export default {
}
},
},
-
- template: `
- <button type="button"
- class="btn stop-env-link has-tooltip"
- data-container="body"
- @click="onClick"
- :disabled="isLoading"
- :title="title"
- :aria-label="title">
- <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
- <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
- </button>
- `,
};
+</script>
+<template>
+ <button
+ type="button"
+ class="btn stop-env-link has-tooltip"
+ data-container="body"
+ @click="onClick"
+ :disabled="isLoading"
+ :title="title"
+ :aria-label="title">
+ <i
+ class="fa fa-stop stop-env-icon"
+ aria-hidden="true" />
+ <i
+ v-if="isLoading"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ </button>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 092a50a0d6f..c8c1f17d4d8 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -1,3 +1,4 @@
+<script>
/**
* Renders a terminal button to open a web terminal.
* Used in environments table.
@@ -24,14 +25,15 @@ export default {
return 'Terminal';
},
},
-
- template: `
- <a class="btn terminal-button has-tooltip"
- data-container="body"
- :title="title"
- :aria-label="title"
- :href="terminalPath">
- ${terminalIconSvg}
- </a>
- `,
};
+</script>
+<template>
+ <a
+ class="btn terminal-button has-tooltip"
+ data-container="body"
+ :title="title"
+ :aria-label="title"
+ :href="terminalPath"
+ v-html="terminalIconSvg">
+ </a>
+</template>
diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js
deleted file mode 100644
index 5e6af3a1d45..00000000000
--- a/app/assets/javascripts/environments/components/environments_table.js
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * Render environments table.
- */
-import EnvironmentTableRowComponent from './environment_item';
-
-export default {
- components: {
- 'environment-item': EnvironmentTableRowComponent,
- },
-
- props: {
- environments: {
- type: Array,
- required: true,
- default: () => ([]),
- },
-
- canReadEnvironment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- canCreateDeployment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- service: {
- type: Object,
- required: true,
- },
-
- isLoadingFolderContent: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- methods: {
- folderUrl(model) {
- return `${window.location.pathname}/folders/${model.folderName}`;
- },
- },
-
- template: `
- <table class="table ci-table">
- <thead>
- <tr>
- <th class="environments-name">Environment</th>
- <th class="environments-deploy">Last deployment</th>
- <th class="environments-build">Job</th>
- <th class="environments-commit">Commit</th>
- <th class="environments-date">Updated</th>
- <th class="environments-actions"></th>
- </tr>
- </thead>
- <tbody>
- <template v-for="model in environments"
- v-bind:model="model">
- <tr is="environment-item"
- :model="model"
- :can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"
- :service="service"></tr>
-
- <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
- <tr v-if="isLoadingFolderContent">
- <td colspan="6" class="text-center">
- <i class="fa fa-spin fa-spinner fa-2x" aria-hidden="true"/>
- </td>
- </tr>
-
- <template v-else>
- <tr is="environment-item"
- v-for="children in model.children"
- :model="children"
- :can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"
- :service="service"></tr>
-
- <tr>
- <td colspan="6" class="text-center">
- <a :href="folderUrl(model)" class="btn btn-default">
- Show all
- </a>
- </td>
- </tr>
- </template>
- </template>
- </template>
- </tbody>
- </table>
- `,
-};
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
new file mode 100644
index 00000000000..87f7cb4a536
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -0,0 +1,117 @@
+<script>
+/**
+ * Render environments table.
+ */
+import EnvironmentTableRowComponent from './environment_item.vue';
+
+export default {
+ components: {
+ 'environment-item': EnvironmentTableRowComponent,
+ },
+
+ props: {
+ environments: {
+ type: Array,
+ required: true,
+ default: () => ([]),
+ },
+
+ canReadEnvironment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ canCreateDeployment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+
+ isLoadingFolderContent: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ methods: {
+ folderUrl(model) {
+ return `${window.location.pathname}/folders/${model.folderName}`;
+ },
+ },
+};
+</script>
+<template>
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="environments-name">
+ Environment
+ </th>
+ <th class="environments-deploy">
+ Last deployment
+ </th>
+ <th class="environments-build">
+ Job
+ </th>
+ <th class="environments-commit">
+ Commit
+ </th>
+ <th class="environments-date">
+ Updated
+ </th>
+ <th class="environments-actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template
+ v-for="model in environments"
+ v-bind:model="model">
+ <tr
+ is="environment-item"
+ :model="model"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ :service="service" />
+
+ <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
+ <tr v-if="isLoadingFolderContent">
+ <td colspan="6" class="text-center">
+ <i
+ class="fa fa-spin fa-spinner fa-2x"
+ aria-hidden="true" />
+ </td>
+ </tr>
+
+ <template v-else>
+ <tr
+ is="environment-item"
+ v-for="children in model.children"
+ :model="children"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ :service="service" />
+
+ <tr>
+ <td
+ colspan="6"
+ class="text-center">
+ <a
+ :href="folderUrl(model)"
+ class="btn btn-default">
+ Show all
+ </a>
+ </td>
+ </tr>
+ </template>
+ </template>
+ </template>
+ </tbody>
+ </table>
+</template>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js
index d2514593e3a..05d44f77d1d 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js
@@ -2,7 +2,7 @@
/* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
-import EnvironmentTable from '../components/environments_table';
+import EnvironmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination';
import '../../lib/utils/common_utils';
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 381c40c03d8..3e7a892756c 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -2,82 +2,80 @@ import Filter from '~/droplab/plugins/filter';
require('./filtered_search_dropdown');
-(() => {
- class DropdownHint extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
- super(droplab, dropdown, input, filter);
- this.config = {
- Filter: {
- template: 'hint',
- filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
- },
- };
- }
-
- itemClicked(e) {
- const { selected } = e.detail;
+class DropdownHint extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ super(droplab, dropdown, input, filter);
+ this.config = {
+ Filter: {
+ template: 'hint',
+ filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
+ },
+ };
+ }
- if (selected.tagName === 'LI') {
- if (selected.hasAttribute('data-value')) {
- this.dismissDropdown();
- } else if (selected.getAttribute('data-action') === 'submit') {
- this.dismissDropdown();
- this.dispatchFormSubmitEvent();
- } else {
- const token = selected.querySelector('.js-filter-hint').innerText.trim();
- const tag = selected.querySelector('.js-filter-tag').innerText.trim();
+ itemClicked(e) {
+ const { selected } = e.detail;
- if (tag.length) {
- // Get previous input values in the input field and convert them into visual tokens
- const previousInputValues = this.input.value.split(' ');
- const searchTerms = [];
+ if (selected.tagName === 'LI') {
+ if (selected.hasAttribute('data-value')) {
+ this.dismissDropdown();
+ } else if (selected.getAttribute('data-action') === 'submit') {
+ this.dismissDropdown();
+ this.dispatchFormSubmitEvent();
+ } else {
+ const token = selected.querySelector('.js-filter-hint').innerText.trim();
+ const tag = selected.querySelector('.js-filter-tag').innerText.trim();
- previousInputValues.forEach((value, index) => {
- searchTerms.push(value);
+ if (tag.length) {
+ // Get previous input values in the input field and convert them into visual tokens
+ const previousInputValues = this.input.value.split(' ');
+ const searchTerms = [];
- if (index === previousInputValues.length - 1
- && token.indexOf(value.toLowerCase()) !== -1) {
- searchTerms.pop();
- }
- });
+ previousInputValues.forEach((value, index) => {
+ searchTerms.push(value);
- if (searchTerms.length > 0) {
- gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
+ if (index === previousInputValues.length - 1
+ && token.indexOf(value.toLowerCase()) !== -1) {
+ searchTerms.pop();
}
+ });
- gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
+ if (searchTerms.length > 0) {
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
}
- this.dismissDropdown();
- this.dispatchInputEvent();
+
+ gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
}
+ this.dismissDropdown();
+ this.dispatchInputEvent();
}
}
+ }
- renderContent() {
- const dropdownData = [];
+ renderContent() {
+ const dropdownData = [];
- [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
- const { icon, hint, tag, type } = dropdownMenu.dataset;
- if (icon && hint && tag) {
- dropdownData.push(
- Object.assign({
- icon: `fa-${icon}`,
- hint,
- tag: `&lt;${tag}&gt;`,
- }, type && { type }),
- );
- }
- });
+ [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
+ const { icon, hint, tag, type } = dropdownMenu.dataset;
+ if (icon && hint && tag) {
+ dropdownData.push(
+ Object.assign({
+ icon: `fa-${icon}`,
+ hint,
+ tag: `&lt;${tag}&gt;`,
+ }, type && { type }),
+ );
+ }
+ });
- this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
- this.droplab.setData(this.hookId, dropdownData);
- }
+ this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
+ this.droplab.setData(this.hookId, dropdownData);
+ }
- init() {
- this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
- }
+ init() {
+ this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
}
+}
- window.gl = window.gl || {};
- gl.DropdownHint = DropdownHint;
-})();
+window.gl = window.gl || {};
+gl.DropdownHint = DropdownHint;
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index 6296965b911..982dc4b61be 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -5,48 +5,46 @@ import Filter from '~/droplab/plugins/filter';
require('./filtered_search_dropdown');
-(() => {
- class DropdownNonUser extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter, endpoint, symbol) {
- super(droplab, dropdown, input, filter);
- this.symbol = symbol;
- this.config = {
- Ajax: {
- endpoint,
- method: 'setData',
- loadingTemplate: this.loadingTemplate,
- onError() {
- /* eslint-disable no-new */
- new Flash('An error occured fetching the dropdown data.');
- /* eslint-enable no-new */
- },
+class DropdownNonUser extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter, endpoint, symbol) {
+ super(droplab, dropdown, input, filter);
+ this.symbol = symbol;
+ this.config = {
+ Ajax: {
+ endpoint,
+ method: 'setData',
+ loadingTemplate: this.loadingTemplate,
+ onError() {
+ /* eslint-disable no-new */
+ new Flash('An error occured fetching the dropdown data.');
+ /* eslint-enable no-new */
},
- Filter: {
- filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
- template: 'title',
- },
- };
- }
+ },
+ Filter: {
+ filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
+ template: 'title',
+ },
+ };
+ }
- itemClicked(e) {
- super.itemClicked(e, (selected) => {
- const title = selected.querySelector('.js-data-value').innerText.trim();
- return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
- });
- }
+ itemClicked(e) {
+ super.itemClicked(e, (selected) => {
+ const title = selected.querySelector('.js-data-value').innerText.trim();
+ return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
+ });
+ }
- renderContent(forceShowList = false) {
- this.droplab
- .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
- super.renderContent(forceShowList);
- }
+ renderContent(forceShowList = false) {
+ this.droplab
+ .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
+ super.renderContent(forceShowList);
+ }
- init() {
- this.droplab
- .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
- }
+ init() {
+ this.droplab
+ .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
}
+}
- window.gl = window.gl || {};
- gl.DropdownNonUser = DropdownNonUser;
-})();
+window.gl = window.gl || {};
+gl.DropdownNonUser = DropdownNonUser;
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 38b5d315bcf..74cec3d75fe 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -4,69 +4,67 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter';
require('./filtered_search_dropdown');
-(() => {
- class DropdownUser extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
- super(droplab, dropdown, input, filter);
- this.config = {
- AjaxFilter: {
- endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
- searchKey: 'search',
- params: {
- per_page: 20,
- active: true,
- project_id: this.getProjectId(),
- current_user: true,
- },
- searchValueFunction: this.getSearchInput.bind(this),
- loadingTemplate: this.loadingTemplate,
- onError() {
- /* eslint-disable no-new */
- new Flash('An error occured fetching the dropdown data.');
- /* eslint-enable no-new */
- },
+class DropdownUser extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ super(droplab, dropdown, input, filter);
+ this.config = {
+ AjaxFilter: {
+ endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
+ searchKey: 'search',
+ params: {
+ per_page: 20,
+ active: true,
+ project_id: this.getProjectId(),
+ current_user: true,
},
- };
- }
-
- itemClicked(e) {
- super.itemClicked(e,
- selected => selected.querySelector('.dropdown-light-content').innerText.trim());
- }
-
- renderContent(forceShowList = false) {
- this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
- super.renderContent(forceShowList);
- }
+ searchValueFunction: this.getSearchInput.bind(this),
+ loadingTemplate: this.loadingTemplate,
+ onError() {
+ /* eslint-disable no-new */
+ new Flash('An error occured fetching the dropdown data.');
+ /* eslint-enable no-new */
+ },
+ },
+ };
+ }
- getProjectId() {
- return this.input.getAttribute('data-project-id');
- }
+ itemClicked(e) {
+ super.itemClicked(e,
+ selected => selected.querySelector('.dropdown-light-content').innerText.trim());
+ }
- getSearchInput() {
- const query = gl.DropdownUtils.getSearchInput(this.input);
- const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
+ renderContent(forceShowList = false) {
+ this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
+ super.renderContent(forceShowList);
+ }
- let value = lastToken || '';
+ getProjectId() {
+ return this.input.getAttribute('data-project-id');
+ }
- if (value[0] === '@') {
- value = value.slice(1);
- }
+ getSearchInput() {
+ const query = gl.DropdownUtils.getSearchInput(this.input);
+ const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
- // Removes the first character if it is a quotation so that we can search
- // with multiple words
- if (value[0] === '"' || value[0] === '\'') {
- value = value.slice(1);
- }
+ let value = lastToken || '';
- return value;
+ if (value[0] === '@') {
+ value = value.slice(1);
}
- init() {
- this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init();
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if (value[0] === '"' || value[0] === '\'') {
+ value = value.slice(1);
}
+
+ return value;
+ }
+
+ init() {
+ this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init();
}
+}
- window.gl = window.gl || {};
- gl.DropdownUser = DropdownUser;
-})();
+window.gl = window.gl || {};
+gl.DropdownUser = DropdownUser;
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 6c5c20447f7..bc7c1dffece 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -1,183 +1,181 @@
import FilteredSearchContainer from './container';
-(() => {
- class DropdownUtils {
- static getEscapedText(text) {
- let escapedText = text;
- const hasSpace = text.indexOf(' ') !== -1;
- const hasDoubleQuote = text.indexOf('"') !== -1;
-
- // Encapsulate value with quotes if it has spaces
- // Known side effect: values's with both single and double quotes
- // won't escape properly
- if (hasSpace) {
- if (hasDoubleQuote) {
- escapedText = `'${text}'`;
- } else {
- // Encapsulate singleQuotes or if it hasSpace
- escapedText = `"${text}"`;
- }
+class DropdownUtils {
+ static getEscapedText(text) {
+ let escapedText = text;
+ const hasSpace = text.indexOf(' ') !== -1;
+ const hasDoubleQuote = text.indexOf('"') !== -1;
+
+ // Encapsulate value with quotes if it has spaces
+ // Known side effect: values's with both single and double quotes
+ // won't escape properly
+ if (hasSpace) {
+ if (hasDoubleQuote) {
+ escapedText = `'${text}'`;
+ } else {
+ // Encapsulate singleQuotes or if it hasSpace
+ escapedText = `"${text}"`;
}
-
- return escapedText;
}
- static filterWithSymbol(filterSymbol, input, item) {
- const updatedItem = item;
- const searchInput = gl.DropdownUtils.getSearchInput(input);
+ return escapedText;
+ }
+
+ static filterWithSymbol(filterSymbol, input, item) {
+ const updatedItem = item;
+ const searchInput = gl.DropdownUtils.getSearchInput(input);
- const title = updatedItem.title.toLowerCase();
- let value = searchInput.toLowerCase();
- let symbol = '';
+ const title = updatedItem.title.toLowerCase();
+ let value = searchInput.toLowerCase();
+ let symbol = '';
- // Remove the symbol for filter
- if (value[0] === filterSymbol) {
- symbol = value[0];
- value = value.slice(1);
- }
+ // Remove the symbol for filter
+ if (value[0] === filterSymbol) {
+ symbol = value[0];
+ value = value.slice(1);
+ }
- // Removes the first character if it is a quotation so that we can search
- // with multiple words
- if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
- value = value.slice(1);
- }
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
+ value = value.slice(1);
+ }
+
+ // Eg. filterSymbol = ~ for labels
+ const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
+ const match = title.indexOf(`${symbol}${value}`) !== -1;
- // Eg. filterSymbol = ~ for labels
- const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
- const match = title.indexOf(`${symbol}${value}`) !== -1;
+ updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
- updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
+ return updatedItem;
+ }
- return updatedItem;
+ static filterHint(input, item) {
+ const updatedItem = item;
+ const searchInput = gl.DropdownUtils.getSearchQuery(input);
+ const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput);
+ const lastKey = lastToken.key || lastToken || '';
+ const allowMultiple = item.type === 'array';
+ const itemInExistingTokens = tokens.some(t => t.key === item.hint);
+
+ if (!allowMultiple && itemInExistingTokens) {
+ updatedItem.droplab_hidden = true;
+ } else if (!lastKey || searchInput.split('').last() === ' ') {
+ updatedItem.droplab_hidden = false;
+ } else if (lastKey) {
+ const split = lastKey.split(':');
+ const tokenName = split[0].split(' ').last();
+
+ const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
+ updatedItem.droplab_hidden = tokenName ? match : false;
}
- static filterHint(input, item) {
- const updatedItem = item;
- const searchInput = gl.DropdownUtils.getSearchQuery(input);
- const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput);
- const lastKey = lastToken.key || lastToken || '';
- const allowMultiple = item.type === 'array';
- const itemInExistingTokens = tokens.some(t => t.key === item.hint);
-
- if (!allowMultiple && itemInExistingTokens) {
- updatedItem.droplab_hidden = true;
- } else if (!lastKey || searchInput.split('').last() === ' ') {
- updatedItem.droplab_hidden = false;
- } else if (lastKey) {
- const split = lastKey.split(':');
- const tokenName = split[0].split(' ').last();
-
- const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
- updatedItem.droplab_hidden = tokenName ? match : false;
- }
+ return updatedItem;
+ }
+
+ static setDataValueIfSelected(filter, selected) {
+ const dataValue = selected.getAttribute('data-value');
- return updatedItem;
+ if (dataValue) {
+ gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
}
- static setDataValueIfSelected(filter, selected) {
- const dataValue = selected.getAttribute('data-value');
+ // Return boolean based on whether it was set
+ return dataValue !== null;
+ }
- if (dataValue) {
- gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
- }
+ // Determines the full search query (visual tokens + input)
+ static getSearchQuery(untilInput = false) {
+ const container = FilteredSearchContainer.container;
+ const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
+ const values = [];
- // Return boolean based on whether it was set
- return dataValue !== null;
+ if (untilInput) {
+ const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
+ // Add one to include input-token to the tokens array
+ tokens.splice(inputIndex + 1);
}
- // Determines the full search query (visual tokens + input)
- static getSearchQuery(untilInput = false) {
- const container = FilteredSearchContainer.container;
- const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
- const values = [];
+ tokens.forEach((token) => {
+ if (token.classList.contains('js-visual-token')) {
+ const name = token.querySelector('.name');
+ const value = token.querySelector('.value');
+ const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
+ let valueText = '';
- if (untilInput) {
- const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
- // Add one to include input-token to the tokens array
- tokens.splice(inputIndex + 1);
- }
+ if (value && value.innerText) {
+ valueText = value.innerText;
+ }
- tokens.forEach((token) => {
- if (token.classList.contains('js-visual-token')) {
- const name = token.querySelector('.name');
- const value = token.querySelector('.value');
- const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
- let valueText = '';
-
- if (value && value.innerText) {
- valueText = value.innerText;
- }
-
- if (token.className.indexOf('filtered-search-token') !== -1) {
- values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
- } else {
- values.push(name.innerText);
- }
- } else if (token.classList.contains('input-token')) {
- const { isLastVisualTokenValid } =
- gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
-
- const input = FilteredSearchContainer.container.querySelector('.filtered-search');
- const inputValue = input && input.value;
-
- if (isLastVisualTokenValid) {
- values.push(inputValue);
- } else {
- const previous = values.pop();
- values.push(`${previous}${inputValue}`);
- }
+ if (token.className.indexOf('filtered-search-token') !== -1) {
+ values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
+ } else {
+ values.push(name.innerText);
}
- });
+ } else if (token.classList.contains('input-token')) {
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- return values
- .map(value => value.trim())
- .join(' ');
- }
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+ const inputValue = input && input.value;
- static getSearchInput(filteredSearchInput) {
- const inputValue = filteredSearchInput.value;
- const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
+ if (isLastVisualTokenValid) {
+ values.push(inputValue);
+ } else {
+ const previous = values.pop();
+ values.push(`${previous}${inputValue}`);
+ }
+ }
+ });
- return inputValue.slice(0, right);
- }
+ return values
+ .map(value => value.trim())
+ .join(' ');
+ }
- static getInputSelectionPosition(input) {
- const selectionStart = input.selectionStart;
- 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)
- // This helps with matching the beginning & end of a token:key
- inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
-
- // Get the right position for the word selected
- // Regex matches first space
- let right = inputValue.slice(selectionStart).search(/\s/);
-
- if (right >= 0) {
- right += selectionStart;
- } else if (right < 0) {
- right = inputValue.length;
- }
+ static getSearchInput(filteredSearchInput) {
+ const inputValue = filteredSearchInput.value;
+ const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
- // Get the left position for the word selected
- // Regex matches last non-whitespace character
- let left = inputValue.slice(0, right).search(/\S+$/);
+ return inputValue.slice(0, right);
+ }
- if (selectionStart === 0) {
- left = 0;
- } else if (selectionStart === inputValue.length && left < 0) {
- left = inputValue.length;
- } else if (left < 0) {
- left = selectionStart;
- }
+ static getInputSelectionPosition(input) {
+ const selectionStart = input.selectionStart;
+ 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)
+ // This helps with matching the beginning & end of a token:key
+ inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
+
+ // Get the right position for the word selected
+ // Regex matches first space
+ let right = inputValue.slice(selectionStart).search(/\s/);
+
+ if (right >= 0) {
+ right += selectionStart;
+ } else if (right < 0) {
+ right = inputValue.length;
+ }
+
+ // Get the left position for the word selected
+ // Regex matches last non-whitespace character
+ let left = inputValue.slice(0, right).search(/\S+$/);
- return {
- left,
- right,
- };
+ if (selectionStart === 0) {
+ left = 0;
+ } else if (selectionStart === inputValue.length && left < 0) {
+ left = inputValue.length;
+ } else if (left < 0) {
+ left = selectionStart;
}
+
+ return {
+ left,
+ right,
+ };
}
+}
- window.gl = window.gl || {};
- gl.DropdownUtils = DropdownUtils;
-})();
+window.gl = window.gl || {};
+gl.DropdownUtils = DropdownUtils;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index d58eeeebf81..4209ca0d6e2 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -1,124 +1,122 @@
-(() => {
- const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
-
- class FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
- this.droplab = droplab;
- this.hookId = input && input.id;
- this.input = input;
- this.filter = filter;
- this.dropdown = dropdown;
- this.loadingTemplate = `<div class="filter-dropdown-loading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>`;
- this.bindEvents();
- }
-
- bindEvents() {
- this.itemClickedWrapper = this.itemClicked.bind(this);
- this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
- }
+const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
+
+class FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ this.droplab = droplab;
+ this.hookId = input && input.id;
+ this.input = input;
+ this.filter = filter;
+ this.dropdown = dropdown;
+ this.loadingTemplate = `<div class="filter-dropdown-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>`;
+ this.bindEvents();
+ }
- unbindEvents() {
- this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
- }
+ bindEvents() {
+ this.itemClickedWrapper = this.itemClicked.bind(this);
+ this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
+ }
- getCurrentHook() {
- return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
- }
+ unbindEvents() {
+ this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
+ }
- itemClicked(e, getValueFunction) {
- const { selected } = e.detail;
+ getCurrentHook() {
+ return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
+ }
- if (selected.tagName === 'LI' && selected.innerHTML) {
- const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
+ itemClicked(e, getValueFunction) {
+ const { selected } = e.detail;
- if (!dataValueSet) {
- const value = getValueFunction(selected);
- gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
- }
+ if (selected.tagName === 'LI' && selected.innerHTML) {
+ const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
- this.resetFilters();
- this.dismissDropdown();
- this.dispatchInputEvent();
+ if (!dataValueSet) {
+ const value = getValueFunction(selected);
+ gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
- }
- setAsDropdown() {
- this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
+ this.resetFilters();
+ this.dismissDropdown();
+ this.dispatchInputEvent();
}
+ }
- setOffset(offset = 0) {
- if (window.innerWidth > 480) {
- this.dropdown.style.left = `${offset}px`;
- } else {
- this.dropdown.style.left = '0px';
- }
+ setAsDropdown() {
+ this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
+ }
+
+ setOffset(offset = 0) {
+ if (window.innerWidth > 480) {
+ this.dropdown.style.left = `${offset}px`;
+ } else {
+ this.dropdown.style.left = '0px';
}
+ }
- renderContent(forceShowList = false) {
- const currentHook = this.getCurrentHook();
- if (forceShowList && currentHook && currentHook.list.hidden) {
- currentHook.list.show();
- }
+ renderContent(forceShowList = false) {
+ const currentHook = this.getCurrentHook();
+ if (forceShowList && currentHook && currentHook.list.hidden) {
+ currentHook.list.show();
}
+ }
- render(forceRenderContent = false, forceShowList = false) {
- this.setAsDropdown();
+ render(forceRenderContent = false, forceShowList = false) {
+ this.setAsDropdown();
- const currentHook = this.getCurrentHook();
- const firstTimeInitialized = currentHook === null;
+ const currentHook = this.getCurrentHook();
+ const firstTimeInitialized = currentHook === null;
- if (firstTimeInitialized || forceRenderContent) {
- this.renderContent(forceShowList);
- } else if (currentHook.list.list.id !== this.dropdown.id) {
- this.renderContent(forceShowList);
- }
+ if (firstTimeInitialized || forceRenderContent) {
+ this.renderContent(forceShowList);
+ } else if (currentHook.list.list.id !== this.dropdown.id) {
+ this.renderContent(forceShowList);
}
+ }
- dismissDropdown() {
- // Focusing on the input will dismiss dropdown
- // (default droplab functionality)
- this.input.focus();
- }
+ dismissDropdown() {
+ // Focusing on the input will dismiss dropdown
+ // (default droplab functionality)
+ this.input.focus();
+ }
- dispatchInputEvent() {
- // Propogate input change to FilteredSearchDropdownManager
- // so that it can determine which dropdowns to open
- this.input.dispatchEvent(new CustomEvent('input', {
- bubbles: true,
- cancelable: true,
- }));
- }
+ dispatchInputEvent() {
+ // Propogate input change to FilteredSearchDropdownManager
+ // so that it can determine which dropdowns to open
+ this.input.dispatchEvent(new CustomEvent('input', {
+ bubbles: true,
+ cancelable: true,
+ }));
+ }
- dispatchFormSubmitEvent() {
- // dispatchEvent() is necessary as form.submit() does not
- // trigger event handlers
- this.input.form.dispatchEvent(new Event('submit'));
- }
+ dispatchFormSubmitEvent() {
+ // dispatchEvent() is necessary as form.submit() does not
+ // trigger event handlers
+ this.input.form.dispatchEvent(new Event('submit'));
+ }
- hideDropdown() {
- const currentHook = this.getCurrentHook();
- if (currentHook) {
- currentHook.list.hide();
- }
+ hideDropdown() {
+ const currentHook = this.getCurrentHook();
+ if (currentHook) {
+ currentHook.list.hide();
}
+ }
- resetFilters() {
- const hook = this.getCurrentHook();
-
- if (hook) {
- const data = hook.list.data || [];
- const results = data.map((o) => {
- const updated = o;
- updated.droplab_hidden = false;
- return updated;
- });
- hook.list.render(results);
- }
+ resetFilters() {
+ const hook = this.getCurrentHook();
+
+ if (hook) {
+ const data = hook.list.data || [];
+ const results = data.map((o) => {
+ const updated = o;
+ updated.droplab_hidden = false;
+ return updated;
+ });
+ hook.list.render(results);
}
}
+}
- window.gl = window.gl || {};
- gl.FilteredSearchDropdown = FilteredSearchDropdown;
-})();
+window.gl = window.gl || {};
+gl.FilteredSearchDropdown = FilteredSearchDropdown;
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 ec481b9ef97..49a6cd1ac77 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -1,191 +1,189 @@
import DropLab from '~/droplab/drop_lab';
import FilteredSearchContainer from './container';
-(() => {
- class FilteredSearchDropdownManager {
- constructor(baseEndpoint = '', page) {
- this.container = FilteredSearchContainer.container;
- this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
- this.tokenizer = gl.FilteredSearchTokenizer;
- this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
- this.filteredSearchInput = this.container.querySelector('.filtered-search');
- this.page = page;
-
- this.setupMapping();
-
- this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('beforeunload', this.cleanupWrapper);
+class FilteredSearchDropdownManager {
+ constructor(baseEndpoint = '', page) {
+ this.container = FilteredSearchContainer.container;
+ this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
+ this.tokenizer = gl.FilteredSearchTokenizer;
+ this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
+ this.filteredSearchInput = this.container.querySelector('.filtered-search');
+ this.page = page;
+
+ this.setupMapping();
+
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ cleanup() {
+ if (this.droplab) {
+ this.droplab.destroy();
+ this.droplab = null;
}
- cleanup() {
- if (this.droplab) {
- this.droplab.destroy();
- this.droplab = null;
- }
+ this.setupMapping();
- this.setupMapping();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
+ }
- document.removeEventListener('beforeunload', this.cleanupWrapper);
- }
+ setupMapping() {
+ this.mapping = {
+ author: {
+ reference: null,
+ gl: 'DropdownUser',
+ element: this.container.querySelector('#js-dropdown-author'),
+ },
+ assignee: {
+ reference: null,
+ gl: 'DropdownUser',
+ element: this.container.querySelector('#js-dropdown-assignee'),
+ },
+ milestone: {
+ reference: null,
+ gl: 'DropdownNonUser',
+ extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
+ element: this.container.querySelector('#js-dropdown-milestone'),
+ },
+ label: {
+ reference: null,
+ gl: 'DropdownNonUser',
+ extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
+ element: this.container.querySelector('#js-dropdown-label'),
+ },
+ hint: {
+ reference: null,
+ gl: 'DropdownHint',
+ element: this.container.querySelector('#js-dropdown-hint'),
+ },
+ };
+ }
- setupMapping() {
- this.mapping = {
- author: {
- reference: null,
- gl: 'DropdownUser',
- element: this.container.querySelector('#js-dropdown-author'),
- },
- assignee: {
- reference: null,
- gl: 'DropdownUser',
- element: this.container.querySelector('#js-dropdown-assignee'),
- },
- milestone: {
- reference: null,
- gl: 'DropdownNonUser',
- extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
- element: this.container.querySelector('#js-dropdown-milestone'),
- },
- label: {
- reference: null,
- gl: 'DropdownNonUser',
- extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
- element: this.container.querySelector('#js-dropdown-label'),
- },
- hint: {
- reference: null,
- gl: 'DropdownHint',
- element: this.container.querySelector('#js-dropdown-hint'),
- },
- };
+ static addWordToInput(tokenName, tokenValue = '', clicked = false) {
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
+ input.value = '';
+
+ if (clicked) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
}
+ }
- static addWordToInput(tokenName, tokenValue = '', clicked = false) {
- const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+ updateCurrentDropdownOffset() {
+ this.updateDropdownOffset(this.currentDropdown);
+ }
- gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
- input.value = '';
+ updateDropdownOffset(key) {
+ // Always align dropdown with the input field
+ let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
- if (clicked) {
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
- }
- }
+ const maxInputWidth = 240;
+ const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
- updateCurrentDropdownOffset() {
- this.updateDropdownOffset(this.currentDropdown);
+ // Make sure offset never exceeds the input container
+ const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
+ if (offsetMaxWidth < offset) {
+ offset = offsetMaxWidth;
}
- updateDropdownOffset(key) {
- // Always align dropdown with the input field
- let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
+ this.mapping[key].reference.setOffset(offset);
+ }
- const maxInputWidth = 240;
- const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
+ load(key, firstLoad = false) {
+ const mappingKey = this.mapping[key];
+ const glClass = mappingKey.gl;
+ const element = mappingKey.element;
+ let forceShowList = false;
- // Make sure offset never exceeds the input container
- const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
- if (offsetMaxWidth < offset) {
- offset = offsetMaxWidth;
- }
+ if (!mappingKey.reference) {
+ const dl = this.droplab;
+ const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
+ const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
- this.mapping[key].reference.setOffset(offset);
+ // Passing glArguments to `new gl[glClass](<arguments>)`
+ mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
}
- load(key, firstLoad = false) {
- const mappingKey = this.mapping[key];
- const glClass = mappingKey.gl;
- const element = mappingKey.element;
- let forceShowList = false;
-
- if (!mappingKey.reference) {
- const dl = this.droplab;
- const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
- const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
+ if (firstLoad) {
+ mappingKey.reference.init();
+ }
- // Passing glArguments to `new gl[glClass](<arguments>)`
- mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
- }
+ if (this.currentDropdown === 'hint') {
+ // Force the dropdown to show if it was clicked from the hint dropdown
+ forceShowList = true;
+ }
- if (firstLoad) {
- mappingKey.reference.init();
- }
+ this.updateDropdownOffset(key);
+ mappingKey.reference.render(firstLoad, forceShowList);
- if (this.currentDropdown === 'hint') {
- // Force the dropdown to show if it was clicked from the hint dropdown
- forceShowList = true;
- }
+ this.currentDropdown = key;
+ }
- this.updateDropdownOffset(key);
- mappingKey.reference.render(firstLoad, forceShowList);
+ loadDropdown(dropdownName = '') {
+ let firstLoad = false;
- this.currentDropdown = key;
+ if (!this.droplab) {
+ firstLoad = true;
+ this.droplab = new DropLab();
}
- loadDropdown(dropdownName = '') {
- let firstLoad = false;
+ const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
+ const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
+ && this.mapping[match.key];
+ const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
- if (!this.droplab) {
- firstLoad = true;
- this.droplab = new DropLab();
- }
+ if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
+ const key = match && match.key ? match.key : 'hint';
+ this.load(key, firstLoad);
+ }
+ }
- const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
- const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
- && this.mapping[match.key];
- const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
+ setDropdown() {
+ const query = gl.DropdownUtils.getSearchQuery(true);
+ const { lastToken, searchToken } = this.tokenizer.processTokens(query);
- if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
- const key = match && match.key ? match.key : 'hint';
- this.load(key, firstLoad);
- }
+ if (this.currentDropdown) {
+ this.updateCurrentDropdownOffset();
}
- setDropdown() {
- const query = gl.DropdownUtils.getSearchQuery(true);
- const { lastToken, searchToken } = this.tokenizer.processTokens(query);
-
- if (this.currentDropdown) {
- this.updateCurrentDropdownOffset();
- }
-
- if (lastToken === searchToken && lastToken !== null) {
- // Token is not fully initialized yet because it has no value
- // Eg. token = 'label:'
-
- const split = lastToken.split(':');
- const dropdownName = split[0].split(' ').last();
- this.loadDropdown(split.length > 1 ? dropdownName : '');
- } else if (lastToken) {
- // Token has been initialized into an object because it has a value
- this.loadDropdown(lastToken.key);
- } else {
- this.loadDropdown('hint');
- }
+ if (lastToken === searchToken && lastToken !== null) {
+ // Token is not fully initialized yet because it has no value
+ // Eg. token = 'label:'
+
+ const split = lastToken.split(':');
+ const dropdownName = split[0].split(' ').last();
+ this.loadDropdown(split.length > 1 ? dropdownName : '');
+ } else if (lastToken) {
+ // Token has been initialized into an object because it has a value
+ this.loadDropdown(lastToken.key);
+ } else {
+ this.loadDropdown('hint');
}
+ }
- resetDropdowns() {
- if (!this.currentDropdown) {
- return;
- }
+ resetDropdowns() {
+ if (!this.currentDropdown) {
+ return;
+ }
- // Force current dropdown to hide
- this.mapping[this.currentDropdown].reference.hideDropdown();
+ // Force current dropdown to hide
+ this.mapping[this.currentDropdown].reference.hideDropdown();
- // Re-Load dropdown
- this.setDropdown();
+ // Re-Load dropdown
+ this.setDropdown();
- // Reset filters for current dropdown
- this.mapping[this.currentDropdown].reference.resetFilters();
+ // Reset filters for current dropdown
+ this.mapping[this.currentDropdown].reference.resetFilters();
- // Reposition dropdown so that it is aligned with cursor
- this.updateDropdownOffset(this.currentDropdown);
- }
+ // Reposition dropdown so that it is aligned with cursor
+ this.updateDropdownOffset(this.currentDropdown);
+ }
- destroyDroplab() {
- this.droplab.destroy();
- }
+ destroyDroplab() {
+ this.droplab.destroy();
}
+}
- window.gl = window.gl || {};
- gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
-})();
+window.gl = window.gl || {};
+gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index b93a8f1d322..68a832102a0 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -6,489 +6,489 @@ import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service';
import eventHub from './event_hub';
-(() => {
- class FilteredSearchManager {
- constructor(page) {
- this.container = FilteredSearchContainer.container;
- this.filteredSearchInput = this.container.querySelector('.filtered-search');
- this.filteredSearchInputForm = this.filteredSearchInput.form;
- this.clearSearchButton = this.container.querySelector('.clear-search');
- this.tokensContainer = this.container.querySelector('.tokens-container');
- this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
-
- this.recentSearchesStore = new RecentSearchesStore();
- let recentSearchesKey = 'issue-recent-searches';
- if (page === 'merge_requests') {
- recentSearchesKey = 'merge-request-recent-searches';
- }
- this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
-
- // Fetch recent searches from localStorage
- this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
- .catch(() => {
- // eslint-disable-next-line no-new
- new Flash('An error occured while parsing recent searches');
- // Gracefully fail to empty array
- return [];
- })
- .then((searches) => {
- // Put any searches that may have come in before
- // we fetched the saved searches ahead of the already saved ones
- const resultantSearches = this.recentSearchesStore.setRecentSearches(
- this.recentSearchesStore.state.recentSearches.concat(searches),
- );
- this.recentSearchesService.save(resultantSearches);
- });
-
- if (this.filteredSearchInput) {
- this.tokenizer = gl.FilteredSearchTokenizer;
- this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
-
- this.recentSearchesRoot = new RecentSearchesRoot(
- this.recentSearchesStore,
- this.recentSearchesService,
- document.querySelector('.js-filtered-search-history-dropdown'),
+class FilteredSearchManager {
+ constructor(page) {
+ this.container = FilteredSearchContainer.container;
+ this.filteredSearchInput = this.container.querySelector('.filtered-search');
+ this.filteredSearchInputForm = this.filteredSearchInput.form;
+ this.clearSearchButton = this.container.querySelector('.clear-search');
+ this.tokensContainer = this.container.querySelector('.tokens-container');
+ this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
+
+ this.recentSearchesStore = new RecentSearchesStore();
+ let recentSearchesKey = 'issue-recent-searches';
+ if (page === 'merge_requests') {
+ recentSearchesKey = 'merge-request-recent-searches';
+ }
+ this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
+
+ // Fetch recent searches from localStorage
+ this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
+ .catch(() => {
+ // eslint-disable-next-line no-new
+ new Flash('An error occured while parsing recent searches');
+ // Gracefully fail to empty array
+ return [];
+ })
+ .then((searches) => {
+ // Put any searches that may have come in before
+ // we fetched the saved searches ahead of the already saved ones
+ const resultantSearches = this.recentSearchesStore.setRecentSearches(
+ this.recentSearchesStore.state.recentSearches.concat(searches),
);
- this.recentSearchesRoot.init();
+ this.recentSearchesService.save(resultantSearches);
+ });
- this.bindEvents();
- this.loadSearchParamsFromURL();
- this.dropdownManager.setDropdown();
+ if (this.filteredSearchInput) {
+ this.tokenizer = gl.FilteredSearchTokenizer;
+ this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
- this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('beforeunload', this.cleanupWrapper);
- }
- }
+ this.recentSearchesRoot = new RecentSearchesRoot(
+ this.recentSearchesStore,
+ this.recentSearchesService,
+ document.querySelector('.js-filtered-search-history-dropdown'),
+ );
+ this.recentSearchesRoot.init();
- cleanup() {
- this.unbindEvents();
- document.removeEventListener('beforeunload', this.cleanupWrapper);
+ this.bindEvents();
+ this.loadSearchParamsFromURL();
+ this.dropdownManager.setDropdown();
- if (this.recentSearchesRoot) {
- this.recentSearchesRoot.destroy();
- }
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
}
+ }
- bindEvents() {
- this.handleFormSubmit = this.handleFormSubmit.bind(this);
- this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
- this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
- this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
- this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
- this.checkForEnterWrapper = this.checkForEnter.bind(this);
- this.onClearSearchWrapper = this.onClearSearch.bind(this);
- this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
- this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
- this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
- this.editTokenWrapper = this.editToken.bind(this);
- this.tokenChange = this.tokenChange.bind(this);
- this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
- this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
- this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
-
- this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
- this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
- this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
- this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
- this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
- this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
- this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
- this.filteredSearchInput.addEventListener('click', this.tokenChange);
- this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
- this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
- this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
- this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
- this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
- document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
- document.addEventListener('click', this.unselectEditTokensWrapper);
- document.addEventListener('click', this.removeInputContainerFocusWrapper);
- document.addEventListener('keydown', this.removeSelectedTokenWrapper);
- eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
- }
+ cleanup() {
+ this.unbindEvents();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
- unbindEvents() {
- this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
- this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
- this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
- this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
- this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
- this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
- this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
- this.filteredSearchInput.removeEventListener('click', this.tokenChange);
- this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
- this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
- this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
- this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
- this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
- document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
- document.removeEventListener('click', this.unselectEditTokensWrapper);
- document.removeEventListener('click', this.removeInputContainerFocusWrapper);
- document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
- eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
+ if (this.recentSearchesRoot) {
+ this.recentSearchesRoot.destroy();
}
+ }
- checkForBackspace(e) {
- // 8 = Backspace Key
- // 46 = Delete Key
- if (e.keyCode === 8 || e.keyCode === 46) {
- const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ bindEvents() {
+ this.handleFormSubmit = this.handleFormSubmit.bind(this);
+ this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
+ this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
+ this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
+ this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
+ this.checkForEnterWrapper = this.checkForEnter.bind(this);
+ this.onClearSearchWrapper = this.onClearSearch.bind(this);
+ this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
+ this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
+ this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
+ this.editTokenWrapper = this.editToken.bind(this);
+ this.tokenChange = this.tokenChange.bind(this);
+ this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
+ this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
+ this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
+
+ this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
+ this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
+ this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
+ this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
+ this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
+ this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
+ this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
+ this.filteredSearchInput.addEventListener('click', this.tokenChange);
+ this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
+ this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
+ this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
+ document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.addEventListener('click', this.unselectEditTokensWrapper);
+ document.addEventListener('click', this.removeInputContainerFocusWrapper);
+ document.addEventListener('keydown', this.removeSelectedTokenWrapper);
+ eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
+ }
- if (this.filteredSearchInput.value === '' && lastVisualToken) {
- this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
- gl.FilteredSearchVisualTokens.removeLastTokenPartial();
- }
+ unbindEvents() {
+ this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
+ this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
+ this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
+ this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
+ this.filteredSearchInput.removeEventListener('click', this.tokenChange);
+ this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
+ this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
+ this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
+ document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.removeEventListener('click', this.unselectEditTokensWrapper);
+ document.removeEventListener('click', this.removeInputContainerFocusWrapper);
+ document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
+ eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
+ }
+
+ checkForBackspace(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- // Reposition dropdown so that it is aligned with cursor
- this.dropdownManager.updateCurrentDropdownOffset();
+ if (this.filteredSearchInput.value === '' && lastVisualToken) {
+ this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
}
- }
- checkForEnter(e) {
- if (e.keyCode === 38 || e.keyCode === 40) {
- const selectionStart = this.filteredSearchInput.selectionStart;
+ // Reposition dropdown so that it is aligned with cursor
+ this.dropdownManager.updateCurrentDropdownOffset();
+ }
+ }
- e.preventDefault();
- this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
- }
+ checkForEnter(e) {
+ if (e.keyCode === 38 || e.keyCode === 40) {
+ const selectionStart = this.filteredSearchInput.selectionStart;
- if (e.keyCode === 13) {
- const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
- const dropdownEl = dropdown.element;
- const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
+ e.preventDefault();
+ this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
+ }
- e.preventDefault();
+ if (e.keyCode === 13) {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+ const dropdownEl = dropdown.element;
+ const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
- if (!activeElements.length) {
- if (this.isHandledAsync) {
- e.stopImmediatePropagation();
+ e.preventDefault();
- this.filteredSearchInput.blur();
- this.dropdownManager.resetDropdowns();
- } else {
- // Prevent droplab from opening dropdown
- this.dropdownManager.destroyDroplab();
- }
+ if (!activeElements.length) {
+ if (this.isHandledAsync) {
+ e.stopImmediatePropagation();
- this.search();
+ this.filteredSearchInput.blur();
+ this.dropdownManager.resetDropdowns();
+ } else {
+ // Prevent droplab from opening dropdown
+ this.dropdownManager.destroyDroplab();
}
+
+ this.search();
}
}
+ }
- addInputContainerFocus() {
- const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
+ addInputContainerFocus() {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
- if (inputContainer) {
- inputContainer.classList.add('focus');
- }
+ if (inputContainer) {
+ inputContainer.classList.add('focus');
}
+ }
- removeInputContainerFocus(e) {
- const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
- const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
- const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
- const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
+ removeInputContainerFocus(e) {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
- if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
- !isElementInStaticFilterDropdown && inputContainer) {
- inputContainer.classList.remove('focus');
- }
+ if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
+ !isElementInStaticFilterDropdown && inputContainer) {
+ inputContainer.classList.remove('focus');
}
+ }
- static selectToken(e) {
- const button = e.target.closest('.selectable');
+ static selectToken(e) {
+ const button = e.target.closest('.selectable');
- if (button) {
- e.preventDefault();
- e.stopPropagation();
- gl.FilteredSearchVisualTokens.selectToken(button);
- }
+ if (button) {
+ e.preventDefault();
+ e.stopPropagation();
+ gl.FilteredSearchVisualTokens.selectToken(button);
}
+ }
- unselectEditTokens(e) {
- const inputContainer = this.container.querySelector('.filtered-search-box');
- const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
- const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
- const isElementTokensContainer = e.target.classList.contains('tokens-container');
+ unselectEditTokens(e) {
+ const inputContainer = this.container.querySelector('.filtered-search-box');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementTokensContainer = e.target.classList.contains('tokens-container');
- if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
- this.dropdownManager.resetDropdowns();
- }
+ if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ this.dropdownManager.resetDropdowns();
}
+ }
- editToken(e) {
- const token = e.target.closest('.js-visual-token');
+ editToken(e) {
+ const token = e.target.closest('.js-visual-token');
- if (token) {
- gl.FilteredSearchVisualTokens.editToken(token);
- this.tokenChange();
- }
+ if (token) {
+ gl.FilteredSearchVisualTokens.editToken(token);
+ this.tokenChange();
}
+ }
- toggleClearSearchButton() {
- const query = gl.DropdownUtils.getSearchQuery();
- const hidden = 'hidden';
- const hasHidden = this.clearSearchButton.classList.contains(hidden);
+ toggleClearSearchButton() {
+ const query = gl.DropdownUtils.getSearchQuery();
+ const hidden = 'hidden';
+ const hasHidden = this.clearSearchButton.classList.contains(hidden);
- if (query.length === 0 && !hasHidden) {
- this.clearSearchButton.classList.add(hidden);
- } else if (query.length && hasHidden) {
- this.clearSearchButton.classList.remove(hidden);
- }
+ if (query.length === 0 && !hasHidden) {
+ this.clearSearchButton.classList.add(hidden);
+ } else if (query.length && hasHidden) {
+ this.clearSearchButton.classList.remove(hidden);
}
+ }
- handleInputPlaceholder() {
- const query = gl.DropdownUtils.getSearchQuery();
- const placeholder = 'Search or filter results...';
- const currentPlaceholder = this.filteredSearchInput.placeholder;
+ handleInputPlaceholder() {
+ const query = gl.DropdownUtils.getSearchQuery();
+ const placeholder = 'Search or filter results...';
+ const currentPlaceholder = this.filteredSearchInput.placeholder;
- if (query.length === 0 && currentPlaceholder !== placeholder) {
- this.filteredSearchInput.placeholder = placeholder;
- } else if (query.length > 0 && currentPlaceholder !== '') {
- this.filteredSearchInput.placeholder = '';
- }
+ if (query.length === 0 && currentPlaceholder !== placeholder) {
+ this.filteredSearchInput.placeholder = placeholder;
+ } else if (query.length > 0 && currentPlaceholder !== '') {
+ this.filteredSearchInput.placeholder = '';
}
+ }
- removeSelectedToken(e) {
- // 8 = Backspace Key
- // 46 = Delete Key
- if (e.keyCode === 8 || e.keyCode === 46) {
- gl.FilteredSearchVisualTokens.removeSelectedToken();
- this.handleInputPlaceholder();
- this.toggleClearSearchButton();
- }
+ removeSelectedToken(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ gl.FilteredSearchVisualTokens.removeSelectedToken();
+ this.handleInputPlaceholder();
+ this.toggleClearSearchButton();
}
+ }
- onClearSearch(e) {
- e.preventDefault();
- this.clearSearch();
- }
+ onClearSearch(e) {
+ e.preventDefault();
+ this.clearSearch();
+ }
- clearSearch() {
- this.filteredSearchInput.value = '';
+ clearSearch() {
+ this.filteredSearchInput.value = '';
- const removeElements = [];
+ const removeElements = [];
- [].forEach.call(this.tokensContainer.children, (t) => {
- if (t.classList.contains('js-visual-token')) {
- removeElements.push(t);
- }
- });
+ [].forEach.call(this.tokensContainer.children, (t) => {
+ if (t.classList.contains('js-visual-token')) {
+ removeElements.push(t);
+ }
+ });
- removeElements.forEach((el) => {
- el.parentElement.removeChild(el);
- });
+ removeElements.forEach((el) => {
+ el.parentElement.removeChild(el);
+ });
- this.clearSearchButton.classList.add('hidden');
- this.handleInputPlaceholder();
+ this.clearSearchButton.classList.add('hidden');
+ this.handleInputPlaceholder();
- this.dropdownManager.resetDropdowns();
+ this.dropdownManager.resetDropdowns();
- if (this.isHandledAsync) {
- this.search();
- }
+ if (this.isHandledAsync) {
+ this.search();
}
+ }
- handleInputVisualToken() {
- const input = this.filteredSearchInput;
- const { tokens, searchToken }
- = gl.FilteredSearchTokenizer.processTokens(input.value);
- const { isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
-
- if (isLastVisualTokenValid) {
- tokens.forEach((t) => {
- input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
- gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
- });
-
- const fragments = searchToken.split(':');
- if (fragments.length > 1) {
- const inputValues = fragments[0].split(' ');
- const tokenKey = inputValues.last();
-
- if (inputValues.length > 1) {
- inputValues.pop();
- const searchTerms = inputValues.join(' ');
-
- input.value = input.value.replace(searchTerms, '');
- gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
- }
+ handleInputVisualToken() {
+ const input = this.filteredSearchInput;
+ const { tokens, searchToken }
+ = gl.FilteredSearchTokenizer.processTokens(input.value);
+ const { isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (isLastVisualTokenValid) {
+ tokens.forEach((t) => {
+ input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
+ });
- gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
- input.value = input.value.replace(`${tokenKey}:`, '');
- }
- } else {
- // Keep listening to token until we determine that the user is done typing the token value
- const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
+ const fragments = searchToken.split(':');
+ if (fragments.length > 1) {
+ const inputValues = fragments[0].split(' ');
+ const tokenKey = inputValues.last();
- if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
- gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
+ if (inputValues.length > 1) {
+ inputValues.pop();
+ const searchTerms = inputValues.join(' ');
- // Trim the last space as seen in the if statement above
- input.value = input.value.replace(searchToken, '').trim();
+ input.value = input.value.replace(searchTerms, '');
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
}
+
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
+ input.value = input.value.replace(`${tokenKey}:`, '');
}
- }
+ } else {
+ // Keep listening to token until we determine that the user is done typing the token value
+ const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
- handleFormSubmit(e) {
- e.preventDefault();
- this.search();
- }
+ if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
- saveCurrentSearchQuery() {
- // Don't save before we have fetched the already saved searches
- this.fetchingRecentSearchesPromise.then(() => {
- const searchQuery = gl.DropdownUtils.getSearchQuery();
- if (searchQuery.length > 0) {
- const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
- this.recentSearchesService.save(resultantSearches);
- }
- });
+ // Trim the last space as seen in the if statement above
+ input.value = input.value.replace(searchToken, '').trim();
+ }
}
+ }
- loadSearchParamsFromURL() {
- const params = gl.utils.getUrlParamsArray();
- const usernameParams = this.getUsernameParams();
- let hasFilteredSearch = false;
+ handleFormSubmit(e) {
+ e.preventDefault();
+ this.search();
+ }
- params.forEach((p) => {
- const split = p.split('=');
- const keyParam = decodeURIComponent(split[0]);
- const value = split[1];
+ saveCurrentSearchQuery() {
+ // Don't save before we have fetched the already saved searches
+ this.fetchingRecentSearchesPromise.then(() => {
+ const searchQuery = gl.DropdownUtils.getSearchQuery();
+ if (searchQuery.length > 0) {
+ const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
+ this.recentSearchesService.save(resultantSearches);
+ }
+ }).catch(() => {
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
+ });
+ }
- // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
- const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
+ loadSearchParamsFromURL() {
+ const params = gl.utils.getUrlParamsArray();
+ const usernameParams = this.getUsernameParams();
+ let hasFilteredSearch = false;
- if (condition) {
- hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
- } else {
- // Sanitize value since URL converts spaces into +
- // Replace before decode so that we know what was originally + versus the encoded +
- const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
- const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
-
- if (match) {
- const indexOf = keyParam.indexOf('_');
- const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
- const symbol = match.symbol;
- let quotationsToUse = '';
-
- if (sanitizedValue.indexOf(' ') !== -1) {
- // Prefer ", but use ' if required
- quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
- }
+ params.forEach((p) => {
+ const split = p.split('=');
+ const keyParam = decodeURIComponent(split[0]);
+ const value = split[1];
+
+ // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
+ const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
+ if (condition) {
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
+ } else {
+ // Sanitize value since URL converts spaces into +
+ // Replace before decode so that we know what was originally + versus the encoded +
+ const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
+ const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
+
+ if (match) {
+ const indexOf = keyParam.indexOf('_');
+ const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
+ const symbol = match.symbol;
+ let quotationsToUse = '';
+
+ if (sanitizedValue.indexOf(' ') !== -1) {
+ // Prefer ", but use ' if required
+ quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
+ }
+
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
+ } else if (!match && keyParam === 'assignee_id') {
+ const id = parseInt(value, 10);
+ if (usernameParams[id]) {
hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
- } else if (!match && keyParam === 'assignee_id') {
- const id = parseInt(value, 10);
- if (usernameParams[id]) {
- hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
- }
- } else if (!match && keyParam === 'author_id') {
- const id = parseInt(value, 10);
- if (usernameParams[id]) {
- hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
- }
- } else if (!match && keyParam === 'search') {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
+ }
+ } else if (!match && keyParam === 'author_id') {
+ const id = parseInt(value, 10);
+ if (usernameParams[id]) {
hasFilteredSearch = true;
- this.filteredSearchInput.value = sanitizedValue;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
}
+ } else if (!match && keyParam === 'search') {
+ hasFilteredSearch = true;
+ this.filteredSearchInput.value = sanitizedValue;
}
- });
+ }
+ });
- this.saveCurrentSearchQuery();
+ this.saveCurrentSearchQuery();
- if (hasFilteredSearch) {
- this.clearSearchButton.classList.remove('hidden');
- this.handleInputPlaceholder();
- }
+ if (hasFilteredSearch) {
+ this.clearSearchButton.classList.remove('hidden');
+ this.handleInputPlaceholder();
}
+ }
- search() {
- const paths = [];
- const searchQuery = gl.DropdownUtils.getSearchQuery();
-
- this.saveCurrentSearchQuery();
+ search() {
+ const paths = [];
+ const searchQuery = gl.DropdownUtils.getSearchQuery();
- const { tokens, searchToken }
- = this.tokenizer.processTokens(searchQuery);
- const currentState = gl.utils.getParameterByName('state') || 'opened';
- paths.push(`state=${currentState}`);
+ this.saveCurrentSearchQuery();
- tokens.forEach((token) => {
- const condition = this.filteredSearchTokenKeys
- .searchByConditionKeyValue(token.key, token.value.toLowerCase());
- const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
- const keyParam = param ? `${token.key}_${param}` : token.key;
- let tokenPath = '';
+ const { tokens, searchToken }
+ = this.tokenizer.processTokens(searchQuery);
+ const currentState = gl.utils.getParameterByName('state') || 'opened';
+ paths.push(`state=${currentState}`);
- if (condition) {
- tokenPath = condition.url;
- } else {
- let tokenValue = token.value;
+ tokens.forEach((token) => {
+ const condition = this.filteredSearchTokenKeys
+ .searchByConditionKeyValue(token.key, token.value.toLowerCase());
+ const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
+ const keyParam = param ? `${token.key}_${param}` : token.key;
+ let tokenPath = '';
- if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
- (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
- tokenValue = tokenValue.slice(1, tokenValue.length - 1);
- }
+ if (condition) {
+ tokenPath = condition.url;
+ } else {
+ let tokenValue = token.value;
- tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
+ if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
+ (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
+ tokenValue = tokenValue.slice(1, tokenValue.length - 1);
}
- paths.push(tokenPath);
- });
-
- if (searchToken) {
- const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
- paths.push(`search=${sanitized}`);
+ tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
}
- const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
+ paths.push(tokenPath);
+ });
- if (this.updateObject) {
- this.updateObject(parameterizedUrl);
- } else {
- gl.utils.visitUrl(parameterizedUrl);
- }
+ if (searchToken) {
+ const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
+ paths.push(`search=${sanitized}`);
}
- getUsernameParams() {
- const usernamesById = {};
- try {
- const attribute = this.filteredSearchInput.getAttribute('data-username-params');
- JSON.parse(attribute).forEach((user) => {
- usernamesById[user.id] = user.username;
- });
- } catch (e) {
- // do nothing
- }
- return usernamesById;
+ const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
+
+ if (this.updateObject) {
+ this.updateObject(parameterizedUrl);
+ } else {
+ gl.utils.visitUrl(parameterizedUrl);
}
+ }
- tokenChange() {
- const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+ getUsernameParams() {
+ const usernamesById = {};
+ try {
+ const attribute = this.filteredSearchInput.getAttribute('data-username-params');
+ JSON.parse(attribute).forEach((user) => {
+ usernamesById[user.id] = user.username;
+ });
+ } catch (e) {
+ // do nothing
+ }
+ return usernamesById;
+ }
- if (dropdown) {
- const currentDropdownRef = dropdown.reference;
+ tokenChange() {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
- this.setDropdownWrapper();
- currentDropdownRef.dispatchInputEvent();
- }
- }
+ if (dropdown) {
+ const currentDropdownRef = dropdown.reference;
- onrecentSearchesItemSelected(text) {
- this.clearSearch();
- this.filteredSearchInput.value = text;
- this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
- this.search();
+ this.setDropdownWrapper();
+ currentDropdownRef.dispatchInputEvent();
}
}
- window.gl = window.gl || {};
- gl.FilteredSearchManager = FilteredSearchManager;
-})();
+ onrecentSearchesItemSelected(text) {
+ this.clearSearch();
+ this.filteredSearchInput.value = text;
+ this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
+ this.search();
+ }
+}
+
+window.gl = window.gl || {};
+gl.FilteredSearchManager = FilteredSearchManager;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index 6d5df86f2a5..1abad9d1b73 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -1,100 +1,98 @@
-(() => {
- const tokenKeys = [{
- key: 'author',
- type: 'string',
- param: 'username',
- symbol: '@',
- }, {
- key: 'assignee',
- type: 'string',
- param: 'username',
- symbol: '@',
- }, {
- key: 'milestone',
- type: 'string',
- param: 'title',
- symbol: '%',
- }, {
- key: 'label',
- type: 'array',
- param: 'name[]',
- symbol: '~',
- }];
+const tokenKeys = [{
+ key: 'author',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+}, {
+ key: 'assignee',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+}, {
+ key: 'milestone',
+ type: 'string',
+ param: 'title',
+ symbol: '%',
+}, {
+ key: 'label',
+ type: 'array',
+ param: 'name[]',
+ symbol: '~',
+}];
- const alternativeTokenKeys = [{
- key: 'label',
- type: 'string',
- param: 'name',
- symbol: '~',
- }];
+const alternativeTokenKeys = [{
+ key: 'label',
+ type: 'string',
+ param: 'name',
+ symbol: '~',
+}];
- const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
+const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
- const conditions = [{
- url: 'assignee_id=0',
- tokenKey: 'assignee',
- value: 'none',
- }, {
- url: 'milestone_title=No+Milestone',
- tokenKey: 'milestone',
- value: 'none',
- }, {
- url: 'milestone_title=%23upcoming',
- tokenKey: 'milestone',
- value: 'upcoming',
- }, {
- url: 'milestone_title=%23started',
- tokenKey: 'milestone',
- value: 'started',
- }, {
- url: 'label_name[]=No+Label',
- tokenKey: 'label',
- value: 'none',
- }];
+const conditions = [{
+ url: 'assignee_id=0',
+ tokenKey: 'assignee',
+ value: 'none',
+}, {
+ url: 'milestone_title=No+Milestone',
+ tokenKey: 'milestone',
+ value: 'none',
+}, {
+ url: 'milestone_title=%23upcoming',
+ tokenKey: 'milestone',
+ value: 'upcoming',
+}, {
+ url: 'milestone_title=%23started',
+ tokenKey: 'milestone',
+ value: 'started',
+}, {
+ url: 'label_name[]=No+Label',
+ tokenKey: 'label',
+ value: 'none',
+}];
- class FilteredSearchTokenKeys {
- static get() {
- return tokenKeys;
- }
+class FilteredSearchTokenKeys {
+ static get() {
+ return tokenKeys;
+ }
- static getAlternatives() {
- return alternativeTokenKeys;
- }
+ static getAlternatives() {
+ return alternativeTokenKeys;
+ }
- static getConditions() {
- return conditions;
- }
+ static getConditions() {
+ return conditions;
+ }
- static searchByKey(key) {
- return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
- }
+ static searchByKey(key) {
+ return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
+ }
- static searchBySymbol(symbol) {
- return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
- }
+ static searchBySymbol(symbol) {
+ return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
+ }
- static searchByKeyParam(keyParam) {
- return tokenKeysWithAlternative.find((tokenKey) => {
- let tokenKeyParam = tokenKey.key;
+ static searchByKeyParam(keyParam) {
+ return tokenKeysWithAlternative.find((tokenKey) => {
+ let tokenKeyParam = tokenKey.key;
- if (tokenKey.param) {
- tokenKeyParam += `_${tokenKey.param}`;
- }
+ if (tokenKey.param) {
+ tokenKeyParam += `_${tokenKey.param}`;
+ }
- return keyParam === tokenKeyParam;
- }) || null;
- }
+ return keyParam === tokenKeyParam;
+ }) || null;
+ }
- static searchByConditionUrl(url) {
- return conditions.find(condition => condition.url === url) || null;
- }
+ static searchByConditionUrl(url) {
+ return conditions.find(condition => condition.url === url) || null;
+ }
- static searchByConditionKeyValue(key, value) {
- return conditions
- .find(condition => condition.tokenKey === key && condition.value === value) || null;
- }
+ static searchByConditionKeyValue(key, value) {
+ return conditions
+ .find(condition => condition.tokenKey === key && condition.value === value) || null;
}
+}
- window.gl = window.gl || {};
- gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
-})();
+window.gl = window.gl || {};
+gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
index a2729dc0e95..2808e4b238a 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
@@ -1,58 +1,56 @@
require('./filtered_search_token_keys');
-(() => {
- class FilteredSearchTokenizer {
- static processTokens(input) {
- const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
- // Regex extracts `(token):(symbol)(value)`
- // Values that start with a double quote must end in a double quote (same for single)
- const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
- const tokens = [];
- const tokenIndexes = []; // stores key+value for simple search
- let lastToken = null;
- const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
- let tokenValue = v1 || v2 || v3;
- let tokenSymbol = symbol;
- let tokenIndex = '';
-
- if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
- tokenSymbol = tokenValue;
- tokenValue = '';
- }
-
- tokenIndex = `${key}:${tokenValue}`;
-
- // Prevent adding duplicates
- if (tokenIndexes.indexOf(tokenIndex) === -1) {
- tokenIndexes.push(tokenIndex);
-
- tokens.push({
- key,
- value: tokenValue || '',
- symbol: tokenSymbol || '',
- });
- }
-
- return '';
- }).replace(/\s{2,}/g, ' ').trim() || '';
-
- if (tokens.length > 0) {
- const last = tokens[tokens.length - 1];
- const lastString = `${last.key}:${last.symbol}${last.value}`;
- lastToken = input.lastIndexOf(lastString) ===
- input.length - lastString.length ? last : searchToken;
- } else {
- lastToken = searchToken;
+class FilteredSearchTokenizer {
+ static processTokens(input) {
+ const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
+ // Regex extracts `(token):(symbol)(value)`
+ // Values that start with a double quote must end in a double quote (same for single)
+ const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
+ const tokens = [];
+ const tokenIndexes = []; // stores key+value for simple search
+ let lastToken = null;
+ const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
+ let tokenValue = v1 || v2 || v3;
+ let tokenSymbol = symbol;
+ let tokenIndex = '';
+
+ if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
+ tokenSymbol = tokenValue;
+ tokenValue = '';
}
- return {
- tokens,
- lastToken,
- searchToken,
- };
+ tokenIndex = `${key}:${tokenValue}`;
+
+ // Prevent adding duplicates
+ if (tokenIndexes.indexOf(tokenIndex) === -1) {
+ tokenIndexes.push(tokenIndex);
+
+ tokens.push({
+ key,
+ value: tokenValue || '',
+ symbol: tokenSymbol || '',
+ });
+ }
+
+ return '';
+ }).replace(/\s{2,}/g, ' ').trim() || '';
+
+ if (tokens.length > 0) {
+ const last = tokens[tokens.length - 1];
+ const lastString = `${last.key}:${last.symbol}${last.value}`;
+ lastToken = input.lastIndexOf(lastString) ===
+ input.length - lastString.length ? last : searchToken;
+ } else {
+ lastToken = searchToken;
}
+
+ return {
+ tokens,
+ lastToken,
+ searchToken,
+ };
}
+}
- window.gl = window.gl || {};
- gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
-})();
+window.gl = window.gl || {};
+gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 9ac4c49d697..b62b2cec4d8 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -50,7 +50,7 @@ window.gl.GfmAutoComplete = {
template: '<li>${title}</li>'
},
Loading: {
- template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+ template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>'
},
DefaultOptions: {
sorter: function(query, items, searchKey) {
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 10363c16bae..acfa4bd4c6b 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var,
+ camelcase, one-var-declaration-per-line, quotes, object-shorthand,
+ prefer-arrow-callback, comma-dangle, consistent-return, yoda,
+ prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
+ promise/catch-or-return */
/* global Api */
var slice = [].slice;
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index de184ab2675..687c2bb6110 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -39,8 +39,9 @@
if ($issuableDueDate.length) {
calendar = new Pikaday({
field: $issuableDueDate.get(0),
- theme: 'gitlab-theme',
+ theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
+ container: $issuableDueDate.parent().get(0),
onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 47e675f537e..011043e992f 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -20,57 +20,60 @@ class Issue {
});
Issue.initIssueBtnEventListeners();
}
+
+ Issue.$btnNewBranch = $('#new-branch');
+
Issue.initMergeRequests();
Issue.initRelatedBranches();
Issue.initCanCreateBranch();
}
static initIssueBtnEventListeners() {
- var issueFailMessage;
- issueFailMessage = 'Unable to update this issue at this time.';
- return $('a.btn-close, a.btn-reopen').on('click', function(e) {
- var $this, isClose, shouldSubmit, url;
+ const issueFailMessage = 'Unable to update this issue at this time.';
+
+ const closeButtons = $('a.btn-close');
+ const isClosedBadge = $('div.status-box-closed');
+ const isOpenBadge = $('div.status-box-open');
+ const projectIssuesCounter = $('.issue_counter');
+ const reopenButtons = $('a.btn-reopen');
+
+ return closeButtons.add(reopenButtons).on('click', function(e) {
+ var $this, shouldSubmit, url;
e.preventDefault();
e.stopImmediatePropagation();
$this = $(this);
- isClose = $this.hasClass('btn-close');
shouldSubmit = $this.hasClass('btn-comment');
if (shouldSubmit) {
Issue.submitNoteForm($this.closest('form'));
}
$this.prop('disabled', true);
+ Issue.setNewBranchButtonState(true, null);
url = $this.attr('href');
return $.ajax({
type: 'PUT',
- url: url,
- error: function(jqXHR, textStatus, errorThrown) {
- var issueStatus;
- issueStatus = isClose ? 'close' : 'open';
- return new Flash(issueFailMessage, 'alert');
- },
- success: function(data, textStatus, jqXHR) {
- if ('id' in data) {
- $(document).trigger('issuable:change');
- let total = Number($('.issue_counter').text().replace(/[^\d]/, ''));
- if (isClose) {
- $('a.btn-close').addClass('hidden');
- $('a.btn-reopen').removeClass('hidden');
- $('div.status-box-closed').removeClass('hidden');
- $('div.status-box-open').addClass('hidden');
- total -= 1;
- } else {
- $('a.btn-reopen').addClass('hidden');
- $('a.btn-close').removeClass('hidden');
- $('div.status-box-closed').addClass('hidden');
- $('div.status-box-open').removeClass('hidden');
- total += 1;
- }
- $('.issue_counter').text(gl.text.addDelimiter(total));
- } else {
- new Flash(issueFailMessage, 'alert');
- }
- return $this.prop('disabled', false);
+ url: url
+ }).fail(function(jqXHR, textStatus, errorThrown) {
+ new Flash(issueFailMessage);
+ Issue.initCanCreateBranch();
+ }).done(function(data, textStatus, jqXHR) {
+ if ('id' in data) {
+ $(document).trigger('issuable:change');
+
+ const isClosed = $this.hasClass('btn-close');
+ closeButtons.toggleClass('hidden', isClosed);
+ reopenButtons.toggleClass('hidden', !isClosed);
+ isClosedBadge.toggleClass('hidden', !isClosed);
+ isOpenBadge.toggleClass('hidden', isClosed);
+
+ let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
+ numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
+ projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
+ } else {
+ new Flash(issueFailMessage);
}
+
+ $this.prop('disabled', false);
+ Issue.initCanCreateBranch();
});
});
}
@@ -86,9 +89,9 @@ class Issue {
static initMergeRequests() {
var $container;
$container = $('#merge-requests');
- return $.getJSON($container.data('url')).error(function() {
- return new Flash('Failed to load referenced merge requests', 'alert');
- }).success(function(data) {
+ return $.getJSON($container.data('url')).fail(function() {
+ return new Flash('Failed to load referenced merge requests');
+ }).done(function(data) {
if ('html' in data) {
return $container.html(data.html);
}
@@ -98,9 +101,9 @@ class Issue {
static initRelatedBranches() {
var $container;
$container = $('#related-branches');
- return $.getJSON($container.data('url')).error(function() {
- return new Flash('Failed to load related branches', 'alert');
- }).success(function(data) {
+ return $.getJSON($container.data('url')).fail(function() {
+ return new Flash('Failed to load related branches');
+ }).done(function(data) {
if ('html' in data) {
return $container.html(data.html);
}
@@ -108,24 +111,27 @@ class Issue {
}
static initCanCreateBranch() {
- var $container;
- $container = $('#new-branch');
// If the user doesn't have the required permissions the container isn't
// rendered at all.
- if ($container.length === 0) {
+ if (Issue.$btnNewBranch.length === 0) {
return;
}
- return $.getJSON($container.data('path')).error(function() {
- $container.find('.unavailable').show();
- return new Flash('Failed to check if a new branch can be created.', 'alert');
- }).success(function(data) {
- if (data.can_create_branch) {
- $container.find('.available').show();
- } else {
- return $container.find('.unavailable').show();
- }
+ return $.getJSON(Issue.$btnNewBranch.data('path')).fail(function() {
+ Issue.setNewBranchButtonState(false, false);
+ new Flash('Failed to check if a new branch can be created.');
+ }).done(function(data) {
+ Issue.setNewBranchButtonState(false, data.can_create_branch);
});
}
+
+ static setNewBranchButtonState(isPending, canCreate) {
+ if (Issue.$btnNewBranch.length === 0) {
+ return;
+ }
+
+ Issue.$btnNewBranch.find('.available').toggle(!isPending && canCreate);
+ Issue.$btnNewBranch.find('.unavailable').toggle(!isPending && !canCreate);
+ }
}
export default Issue;
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index b6ce8e83729..4d491e70d83 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,26 +1,20 @@
import Vue from 'vue';
-import IssueTitle from './issue_title';
+import IssueTitle from './issue_title.vue';
import '../vue_shared/vue_resource_interceptor';
-const vueOptions = () => ({
- el: '.issue-title-entrypoint',
- components: {
- IssueTitle,
- },
- data() {
- const issueTitleData = document.querySelector('.issue-title-data').dataset;
+(() => {
+ const issueTitleData = document.querySelector('.issue-title-data').dataset;
+ const { initialTitle, endpoint } = issueTitleData;
- return {
- initialTitle: issueTitleData.initialTitle,
- endpoint: issueTitleData.endpoint,
- };
- },
- template: `
- <IssueTitle
- :initialTitle="initialTitle"
- :endpoint="endpoint"
- />
- `,
-});
+ const vm = new Vue({
+ el: '.issue-title-entrypoint',
+ render: createElement => createElement(IssueTitle, {
+ props: {
+ initialTitle,
+ endpoint,
+ },
+ }),
+ });
-(() => new Vue(vueOptions()))();
+ return vm;
+})();
diff --git a/app/assets/javascripts/issue_show/issue_title.js b/app/assets/javascripts/issue_show/issue_title.vue
index 1184c8956dc..00b0e56030a 100644
--- a/app/assets/javascripts/issue_show/issue_title.js
+++ b/app/assets/javascripts/issue_show/issue_title.vue
@@ -1,3 +1,4 @@
+<script>
import Visibility from 'visibilityjs';
import Poll from './../lib/utils/poll';
import Service from './services/index';
@@ -33,17 +34,6 @@ export default {
};
},
methods: {
- fetch() {
- this.poll.makeRequest();
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- this.poll.restart();
- } else {
- this.poll.stop();
- }
- });
- },
renderResponse(res) {
const body = JSON.parse(res.body);
this.triggerAnimation(body);
@@ -70,9 +60,21 @@ export default {
},
},
created() {
- this.fetch();
+ if (!Visibility.hidden()) {
+ this.poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
},
- template: `
- <h2 class='title' v-html='title'></h2>
- `,
};
+</script>
+
+<template>
+ <h2 class="title" v-html="title"></h2>
+</template>
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 443fb3e0ca9..9a60f5464df 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -332,6 +332,9 @@
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e, isMarking) {
var isIssueIndex, isMRIndex, page, boardsModel;
+ var fadeOutLoader = () => {
+ $loading.fadeOut();
+ };
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
@@ -396,9 +399,8 @@
$loading.fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
- .then(function () {
- $loading.fadeOut();
- });
+ .then(fadeOutLoader)
+ .catch(fadeOutLoader);
}
else {
if ($dropdown.hasClass('js-multiselect')) {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index e1e6ca25446..8058672eaa9 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -47,6 +47,10 @@
}
};
+ gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) {
+ return $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
+ };
+
w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
event_name = event_name || 'input';
var closest_submit, field, that;
@@ -165,7 +169,10 @@
w.gl.utils.getSelectedFragment = () => {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
- const documentFragment = selection.getRangeAt(0).cloneContents();
+ const documentFragment = document.createDocumentFragment();
+ for (let i = 0; i < selection.rangeCount; i += 1) {
+ documentFragment.appendChild(selection.getRangeAt(i).cloneContents());
+ }
if (documentFragment.textContent.length === 0) return null;
return documentFragment;
@@ -364,9 +371,9 @@
});
};
- w.gl.utils.setFavicon = (iconName) => {
- if (faviconEl && iconName) {
- faviconEl.setAttribute('href', `/assets/${iconName}.ico`);
+ w.gl.utils.setFavicon = (faviconPath) => {
+ if (faviconEl && faviconPath) {
+ faviconEl.setAttribute('href', faviconPath);
}
};
@@ -381,8 +388,8 @@
url: pageUrl,
dataType: 'json',
success: function(data) {
- if (data && data.icon) {
- gl.utils.setFavicon(`ci_favicons/${data.icon}`);
+ if (data && data.favicon) {
+ gl.utils.setFavicon(data.favicon);
} else {
gl.utils.resetFavicon();
}
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
new file mode 100644
index 00000000000..1e96c7ab5cd
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -0,0 +1,2 @@
+/* eslint-disable import/prefer-default-export */
+export const BYTES_IN_KIB = 1024;
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index e2bf69ee52e..f1b07408671 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -1,4 +1,4 @@
-/* eslint-disable import/prefer-default-export */
+import { BYTES_IN_KIB } from './constants';
/**
* Function that allows a number with an X amount of decimals
@@ -32,3 +32,13 @@ export function formatRelevantDigits(number) {
}
return formattedNumber;
}
+
+/**
+ * Utility function that calculates KiB of the given bytes.
+ *
+ * @param {Number} number bytes
+ * @return {Number} KiB
+ */
+export function bytesToKiB(number) {
+ return number / BYTES_IN_KIB;
+}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 2e5f8a09fc1..fecd531328d 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,192 +1,188 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */
-
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
require('vendor/latinise');
-(function() {
- (function(w) {
- var base;
- if (w.gl == null) {
- w.gl = {};
- }
- if ((base = w.gl).text == null) {
- base.text = {};
+var base;
+var w = window;
+if (w.gl == null) {
+ w.gl = {};
+}
+if ((base = w.gl).text == null) {
+ base.text = {};
+}
+gl.text.addDelimiter = function(text) {
+ return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
+};
+gl.text.highCountTrim = function(count) {
+ return count > 99 ? '99+' : count;
+};
+gl.text.randomString = function() {
+ return Math.random().toString(36).substring(7);
+};
+gl.text.replaceRange = function(s, start, end, substitute) {
+ return s.substring(0, start) + substitute + s.substring(end);
+};
+gl.text.getTextWidth = function(text, font) {
+ /**
+ * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
+ *
+ * @param {String} text The text to be rendered.
+ * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
+ *
+ * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
+ */
+ // re-use canvas object for better performance
+ var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
+ var context = canvas.getContext('2d');
+ context.font = font;
+ return context.measureText(text).width;
+};
+gl.text.selectedText = function(text, textarea) {
+ return text.substring(textarea.selectionStart, textarea.selectionEnd);
+};
+gl.text.lineBefore = function(text, textarea) {
+ var split;
+ split = text.substring(0, textarea.selectionStart).trim().split('\n');
+ return split[split.length - 1];
+};
+gl.text.lineAfter = function(text, textarea) {
+ return text.substring(textarea.selectionEnd).trim().split('\n')[0];
+};
+gl.text.blockTagText = function(text, textArea, blockTag, selected) {
+ var lineAfter, lineBefore;
+ lineBefore = this.lineBefore(text, textArea);
+ lineAfter = this.lineAfter(text, textArea);
+ if (lineBefore === blockTag && lineAfter === blockTag) {
+ // To remove the block tag we have to select the line before & after
+ if (blockTag != null) {
+ textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
+ textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
}
- gl.text.addDelimiter = function(text) {
- return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
- };
- gl.text.highCountTrim = function(count) {
- return count > 99 ? '99+' : count;
- };
- gl.text.randomString = function() {
- return Math.random().toString(36).substring(7);
- };
- gl.text.replaceRange = function(s, start, end, substitute) {
- return s.substring(0, start) + substitute + s.substring(end);
- };
- gl.text.getTextWidth = function(text, font) {
- /**
- * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
- *
- * @param {String} text The text to be rendered.
- * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
- *
- * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
- */
- // re-use canvas object for better performance
- var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
- var context = canvas.getContext('2d');
- context.font = font;
- return context.measureText(text).width;
- };
- gl.text.selectedText = function(text, textarea) {
- return text.substring(textarea.selectionStart, textarea.selectionEnd);
- };
- gl.text.lineBefore = function(text, textarea) {
- var split;
- split = text.substring(0, textarea.selectionStart).trim().split('\n');
- return split[split.length - 1];
- };
- gl.text.lineAfter = function(text, textarea) {
- return text.substring(textarea.selectionEnd).trim().split('\n')[0];
- };
- gl.text.blockTagText = function(text, textArea, blockTag, selected) {
- var lineAfter, lineBefore;
- lineBefore = this.lineBefore(text, textArea);
- lineAfter = this.lineAfter(text, textArea);
- if (lineBefore === blockTag && lineAfter === blockTag) {
- // To remove the block tag we have to select the line before & after
- if (blockTag != null) {
- textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
- textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
- }
- return selected;
- } else {
- return blockTag + "\n" + selected + "\n" + blockTag;
- }
- };
- gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
- var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
- removedLastNewLine = false;
- removedFirstNewLine = false;
- currentLineEmpty = false;
+ return selected;
+ } else {
+ return blockTag + "\n" + selected + "\n" + blockTag;
+ }
+};
+gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
+ var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
+ removedLastNewLine = false;
+ removedFirstNewLine = false;
+ currentLineEmpty = false;
- // Remove the first newline
- if (selected.indexOf('\n') === 0) {
- removedFirstNewLine = true;
- selected = selected.replace(/\n+/, '');
- }
+ // Remove the first newline
+ if (selected.indexOf('\n') === 0) {
+ removedFirstNewLine = true;
+ selected = selected.replace(/\n+/, '');
+ }
- // Remove the last newline
- if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
- removedLastNewLine = true;
- selected = selected.replace(/\n$/, '');
- }
+ // Remove the last newline
+ if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
+ removedLastNewLine = true;
+ selected = selected.replace(/\n$/, '');
+ }
- selectedSplit = selected.split('\n');
+ selectedSplit = selected.split('\n');
- if (!wrap) {
- lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
+ if (!wrap) {
+ lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
- // Check whether the current line is empty or consists only of spaces(=handle as empty)
- if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
- currentLineEmpty = true;
- }
- }
+ // Check whether the current line is empty or consists only of spaces(=handle as empty)
+ if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
+ currentLineEmpty = true;
+ }
+ }
- startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+ startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
- if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
- if (blockTag != null) {
- insertText = this.blockTagText(text, textArea, blockTag, selected);
+ if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
+ if (blockTag != null) {
+ insertText = this.blockTagText(text, textArea, blockTag, selected);
+ } else {
+ insertText = selectedSplit.map(function(val) {
+ if (val.indexOf(tag) === 0) {
+ return "" + (val.replace(tag, ''));
} else {
- insertText = selectedSplit.map(function(val) {
- if (val.indexOf(tag) === 0) {
- return "" + (val.replace(tag, ''));
- } else {
- return "" + tag + val;
- }
- }).join('\n');
+ return "" + tag + val;
}
- } else {
- insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
- }
+ }).join('\n');
+ }
+ } else {
+ insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
+ }
- if (removedFirstNewLine) {
- insertText = '\n' + insertText;
- }
+ if (removedFirstNewLine) {
+ insertText = '\n' + insertText;
+ }
- if (removedLastNewLine) {
- insertText += '\n';
- }
+ if (removedLastNewLine) {
+ insertText += '\n';
+ }
- if (document.queryCommandSupported('insertText')) {
- inserted = document.execCommand('insertText', false, insertText);
- }
- if (!inserted) {
- try {
- document.execCommand("ms-beginUndoUnit");
- } catch (error) {}
- textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
- try {
- document.execCommand("ms-endUndoUnit");
- } catch (error) {}
- }
- return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
- };
- gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
- var pos;
- if (!textArea.setSelectionRange) {
- return;
- }
- if (textArea.selectionStart === textArea.selectionEnd) {
- if (wrapped) {
- pos = textArea.selectionStart - tag.length;
- } else {
- pos = textArea.selectionStart;
- }
+ if (document.queryCommandSupported('insertText')) {
+ inserted = document.execCommand('insertText', false, insertText);
+ }
+ if (!inserted) {
+ try {
+ document.execCommand("ms-beginUndoUnit");
+ } catch (error) {}
+ textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
+ try {
+ document.execCommand("ms-endUndoUnit");
+ } catch (error) {}
+ }
+ return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
+};
+gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
+ var pos;
+ if (!textArea.setSelectionRange) {
+ return;
+ }
+ if (textArea.selectionStart === textArea.selectionEnd) {
+ if (wrapped) {
+ pos = textArea.selectionStart - tag.length;
+ } else {
+ pos = textArea.selectionStart;
+ }
- if (removedLastNewLine) {
- pos -= 1;
- }
+ if (removedLastNewLine) {
+ pos -= 1;
+ }
- return textArea.setSelectionRange(pos, pos);
- }
- };
- gl.text.updateText = function(textArea, tag, blockTag, wrap) {
- var $textArea, selected, text;
- $textArea = $(textArea);
- textArea = $textArea.get(0);
- text = $textArea.val();
- selected = this.selectedText(text, textArea);
- $textArea.focus();
- return this.insertText(textArea, text, tag, blockTag, selected, wrap);
- };
- gl.text.init = function(form) {
- var self;
- self = this;
- return $('.js-md', form).off('click').on('click', function() {
- var $this;
- $this = $(this);
- return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
- });
- };
- gl.text.removeListeners = function(form) {
- return $('.js-md', form).off();
- };
- gl.text.humanize = function(string) {
- return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
- };
- gl.text.pluralize = function(str, count) {
- return str + (count > 1 || count === 0 ? 's' : '');
- };
- gl.text.truncate = function(string, maxLength) {
- return string.substr(0, (maxLength - 3)) + '...';
- };
- gl.text.dasherize = function(str) {
- return str.replace(/[_\s]+/g, '-');
- };
- gl.text.slugify = function(str) {
- return str.trim().toLowerCase().latinise();
- };
- })(window);
-}).call(window);
+ return textArea.setSelectionRange(pos, pos);
+ }
+};
+gl.text.updateText = function(textArea, tag, blockTag, wrap) {
+ var $textArea, selected, text;
+ $textArea = $(textArea);
+ textArea = $textArea.get(0);
+ text = $textArea.val();
+ selected = this.selectedText(text, textArea);
+ $textArea.focus();
+ return this.insertText(textArea, text, tag, blockTag, selected, wrap);
+};
+gl.text.init = function(form) {
+ var self;
+ self = this;
+ return $('.js-md', form).off('click').on('click', function() {
+ var $this;
+ $this = $(this);
+ return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
+ });
+};
+gl.text.removeListeners = function(form) {
+ return $('.js-md', form).off();
+};
+gl.text.humanize = function(string) {
+ return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
+};
+gl.text.pluralize = function(str, count) {
+ return str + (count > 1 || count === 0 ? 's' : '');
+};
+gl.text.truncate = function(string, maxLength) {
+ return string.substr(0, (maxLength - 3)) + '...';
+};
+gl.text.dasherize = function(str) {
+ return str.replace(/[_\s]+/g, '-');
+};
+gl.text.slugify = function(str) {
+ return str.trim().toLowerCase().latinise();
+};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 09c4261b318..b9d2fc25c39 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,93 +1,90 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
-(function() {
- (function(w) {
- var base;
- if (w.gl == null) {
- w.gl = {};
+var base;
+var w = window;
+if (w.gl == null) {
+ w.gl = {};
+}
+if ((base = w.gl).utils == null) {
+ base.utils = {};
+}
+// Returns an array containing the value(s) of the
+// of the key passed as an argument
+w.gl.utils.getParameterValues = function(sParam) {
+ var i, sPageURL, sParameterName, sURLVariables, values;
+ sPageURL = decodeURIComponent(window.location.search.substring(1));
+ sURLVariables = sPageURL.split('&');
+ sParameterName = void 0;
+ values = [];
+ i = 0;
+ while (i < sURLVariables.length) {
+ sParameterName = sURLVariables[i].split('=');
+ if (sParameterName[0] === sParam) {
+ values.push(sParameterName[1].replace(/\+/g, ' '));
}
- if ((base = w.gl).utils == null) {
- base.utils = {};
+ i += 1;
+ }
+ return values;
+};
+// @param {Object} params - url keys and value to merge
+// @param {String} url
+w.gl.utils.mergeUrlParams = function(params, url) {
+ var lastChar, newUrl, paramName, paramValue, pattern;
+ newUrl = decodeURIComponent(url);
+ for (paramName in params) {
+ paramValue = params[paramName];
+ pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
+ if (paramValue == null) {
+ newUrl = newUrl.replace(pattern, '');
+ } else if (url.search(pattern) !== -1) {
+ newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
+ } else {
+ newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
}
- // Returns an array containing the value(s) of the
- // of the key passed as an argument
- w.gl.utils.getParameterValues = function(sParam) {
- var i, sPageURL, sParameterName, sURLVariables, values;
- sPageURL = decodeURIComponent(window.location.search.substring(1));
- sURLVariables = sPageURL.split('&');
- sParameterName = void 0;
- values = [];
- i = 0;
- while (i < sURLVariables.length) {
- sParameterName = sURLVariables[i].split('=');
- if (sParameterName[0] === sParam) {
- values.push(sParameterName[1].replace(/\+/g, ' '));
- }
- i += 1;
+ }
+ // Remove a trailing ampersand
+ lastChar = newUrl[newUrl.length - 1];
+ if (lastChar === '&') {
+ newUrl = newUrl.slice(0, -1);
+ }
+ return newUrl;
+};
+// removes parameter query string from url. returns the modified url
+w.gl.utils.removeParamQueryString = function(url, param) {
+ var urlVariables, variables;
+ url = decodeURIComponent(url);
+ urlVariables = url.split('&');
+ return ((function() {
+ var j, len, results;
+ results = [];
+ for (j = 0, len = urlVariables.length; j < len; j += 1) {
+ variables = urlVariables[j];
+ if (variables.indexOf(param) === -1) {
+ results.push(variables);
}
- return values;
- };
- // @param {Object} params - url keys and value to merge
- // @param {String} url
- w.gl.utils.mergeUrlParams = function(params, url) {
- var lastChar, newUrl, paramName, paramValue, pattern;
- newUrl = decodeURIComponent(url);
- for (paramName in params) {
- paramValue = params[paramName];
- pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
- if (paramValue == null) {
- newUrl = newUrl.replace(pattern, '');
- } else if (url.search(pattern) !== -1) {
- newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
- } else {
- newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
- }
- }
- // Remove a trailing ampersand
- lastChar = newUrl[newUrl.length - 1];
- if (lastChar === '&') {
- newUrl = newUrl.slice(0, -1);
- }
- return newUrl;
- };
- // removes parameter query string from url. returns the modified url
- w.gl.utils.removeParamQueryString = function(url, param) {
- var urlVariables, variables;
- url = decodeURIComponent(url);
- urlVariables = url.split('&');
- return ((function() {
- var j, len, results;
- results = [];
- for (j = 0, len = urlVariables.length; j < len; j += 1) {
- variables = urlVariables[j];
- if (variables.indexOf(param) === -1) {
- results.push(variables);
- }
- }
- return results;
- })()).join('&');
- };
- w.gl.utils.removeParams = (params) => {
- const url = new URL(window.location.href);
- params.forEach((param) => {
- url.search = w.gl.utils.removeParamQueryString(url.search, param);
- });
- return url.href;
- };
- w.gl.utils.getLocationHash = function(url) {
- var hashIndex;
- if (typeof url === 'undefined') {
- // Note: We can't use window.location.hash here because it's
- // not consistent across browsers - Firefox will pre-decode it
- url = window.location.href;
- }
- hashIndex = url.indexOf('#');
- return hashIndex === -1 ? null : url.substring(hashIndex + 1);
- };
+ }
+ return results;
+ })()).join('&');
+};
+w.gl.utils.removeParams = (params) => {
+ const url = new URL(window.location.href);
+ params.forEach((param) => {
+ url.search = w.gl.utils.removeParamQueryString(url.search, param);
+ });
+ return url.href;
+};
+w.gl.utils.getLocationHash = function(url) {
+ var hashIndex;
+ if (typeof url === 'undefined') {
+ // Note: We can't use window.location.hash here because it's
+ // not consistent across browsers - Firefox will pre-decode it
+ url = window.location.href;
+ }
+ hashIndex = url.indexOf('#');
+ return hashIndex === -1 ? null : url.substring(hashIndex + 1);
+};
- w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
+w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
- w.gl.utils.visitUrl = (url) => {
- document.location.href = url;
- };
- })(window);
-}).call(window);
+w.gl.utils.visitUrl = (url) => {
+ document.location.href = url;
+};
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index c50ec24c818..be3c2c9fbb1 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -165,6 +165,7 @@ import './syntax_highlight';
import './task_list';
import './todos';
import './tree';
+import './usage_ping';
import './user';
import './user_tabs';
import './username_validator';
@@ -210,6 +211,14 @@ $(function () {
}
});
+ if (bootstrapBreakpoint === 'xs') {
+ const $rightSidebar = $('aside.right-sidebar, .page-with-sidebar');
+
+ $rightSidebar
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed');
+ }
+
// prevent default action for disabled buttons
$('.btn').click(function(e) {
if ($(this).hasClass('disabled')) {
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index 129d2dc5f0a..e034729bd39 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -18,9 +18,10 @@
const calendar = new Pikaday({
field: $input.get(0),
- theme: 'gitlab-theme',
+ theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
minDate: new Date(),
+ container: $input.parent().get(0),
onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index b0254b17dd2..42ecf0d6cb2 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -157,7 +157,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
$('.ci-widget-fetching').show();
return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) {
- var message, status, title;
+ var message, status, title, callback;
_this.status = data.status;
_this.hasCi = data.has_ci;
_this.updateMergeButton(_this.status, _this.hasCi);
@@ -179,6 +179,12 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.opts.ci_sha = data.sha;
_this.updateCommitUrls(data.sha);
}
+ if (data.status === "success" || data.status === "failed") {
+ callback = function() {
+ return _this.getMergeStatus();
+ };
+ return setTimeout(callback, 2000);
+ }
if (showNotification && data.status) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 9548a98f499..7b0997c6520 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -1,11 +1,13 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len */
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+import '~/lib/utils/url_utility';
+(function() {
this.MergedButtons = (function() {
function MergedButtons() {
- this.removeSourceBranch = bind(this.removeSourceBranch, this);
+ this.removeSourceBranch = this.removeSourceBranch.bind(this);
+ this.removeBranchSuccess = this.removeBranchSuccess.bind(this);
+ this.removeBranchError = this.removeBranchError.bind(this);
this.$removeBranchWidget = $('.remove_source_branch_widget');
this.$removeBranchProgress = $('.remove_source_branch_in_progress');
this.$removeBranchFailed = $('.remove_source_branch_widget.failed');
@@ -22,7 +24,7 @@
MergedButtons.prototype.initEventListeners = function() {
$(document).on('click', '.remove_source_branch', this.removeSourceBranch);
$(document).on('ajax:success', '.remove_source_branch', this.removeBranchSuccess);
- return $(document).on('ajax:error', '.remove_source_branch', this.removeBranchError);
+ $(document).on('ajax:error', '.remove_source_branch', this.removeBranchError);
};
MergedButtons.prototype.removeSourceBranch = function() {
@@ -31,7 +33,7 @@
};
MergedButtons.prototype.removeBranchSuccess = function() {
- return location.reload();
+ gl.utils.refreshCurrentPage();
};
MergedButtons.prototype.removeBranchError = function() {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 773fe3233a7..bebd0aa357e 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -164,6 +164,9 @@
.then(function () {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
+ })
+ .catch(() => {
+ $loading.fadeOut();
});
} else {
selected = $selectbox.find('input[type="hidden"]').val();
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index d82a4eb9642..aff507abb91 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -71,6 +71,8 @@ class PrometheusGraph {
this.transformData(metricsResponse);
this.createGraph();
}
+ }).catch(() => {
+ new Flash('An error occurred when trying to load metrics. Please try again.');
});
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 15f7a813626..974fb0d83da 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -308,8 +308,10 @@ require('./task_list');
if (this.isNewNote(note)) {
this.note_ids.push(note.id);
- $notesList = $('ul.main-notes-list');
- $notesList.append(note.html).syntaxHighlight();
+
+ $notesList = window.$('ul.main-notes-list');
+ Notes.animateAppendNote(note.html, $notesList);
+
// Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.collapseLongCommitList();
@@ -348,7 +350,7 @@ require('./task_list');
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
- discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
+ discussionContainer = window.$(`.notes[data-discussion-id="${note.discussion_id}"]`);
if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes');
}
@@ -370,14 +372,13 @@ require('./task_list');
row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
}
}
-
// Init discussion on 'Discussion' page if it is merge request page
- if ($('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
- $('ul.main-notes-list').append($(note.discussion_html).renderGFM());
+ if (window.$('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
+ Notes.animateAppendNote(note.discussion_html, window.$('ul.main-notes-list'));
}
} else {
// append new note to all matching discussions
- discussionContainer.append($(note.html).renderGFM());
+ Notes.animateAppendNote(note.html, discussionContainer);
}
if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) {
@@ -1063,6 +1064,13 @@ require('./task_list');
return $form;
};
+ Notes.animateAppendNote = function(noteHTML, $notesList) {
+ const $note = window.$(noteHTML);
+
+ $note.addClass('fade-in').renderGFM();
+ $notesList.append($note);
+ };
+
return Notes;
})();
}).call(window);
diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/pipelines/components/async_button.vue
index 58b8db4d519..d1c60b570de 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/async_button.js
+++ b/app/assets/javascripts/pipelines/components/async_button.vue
@@ -1,3 +1,4 @@
+<script>
/* eslint-disable no-new, no-alert */
/* global Flash */
import '~/flash';
@@ -64,30 +65,38 @@ export default {
makeRequest() {
this.isLoading = true;
+ $(this.$el).tooltip('destroy');
+
this.service.postAction(this.endpoint)
- .then(() => {
- this.isLoading = false;
- eventHub.$emit('refreshPipelines');
- })
- .catch(() => {
- this.isLoading = false;
- new Flash('An error occured while making the request.');
- });
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshPipelines');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
},
},
-
- template: `
- <button
- type="button"
- @click="onClick"
- :class="buttonClass"
- :title="title"
- :aria-label="title"
- data-container="body"
- data-placement="top"
- :disabled="isLoading">
- <i :class="iconClass" aria-hidden="true"/>
- <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
- </button>
- `,
};
+</script>
+
+<template>
+ <button
+ type="button"
+ @click="onClick"
+ :class="buttonClass"
+ :title="title"
+ :aria-label="title"
+ data-container="body"
+ data-placement="top"
+ :disabled="isLoading">
+ <i
+ :class="iconClass"
+ aria-hidden="true" />
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ v-if="isLoading" />
+ </button>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
new file mode 100644
index 00000000000..3db64339a62
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -0,0 +1,34 @@
+<script>
+import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg';
+
+export default {
+ props: {
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ data: () => ({ pipelinesEmptyStateSVG }),
+};
+</script>
+
+<template>
+ <div class="row empty-state js-empty-state">
+ <div class="col-xs-12">
+ <div class="svg-content" v-html="pipelinesEmptyStateSVG" />
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>Build with confidence</h4>
+ <p>
+ Continous Integration can help catch bugs by running your tests automatically,
+ while Continuous Deployment can help you deliver code to your product environment.
+ </p>
+ <a :href="helpPagePath" class="btn btn-info">
+ Get started with Pipelines
+ </a>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue
new file mode 100644
index 00000000000..90cee68163e
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/error_state.vue
@@ -0,0 +1,21 @@
+<script>
+import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg';
+
+export default {
+ data: () => ({ pipelinesErrorStateSVG }),
+};
+</script>
+
+<template>
+ <div class="row empty-state js-pipelines-error-state">
+ <div class="col-xs-12">
+ <div class="svg-content" v-html="pipelinesErrorStateSVG" />
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>The API failed to fetch the pipelines.</h4>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js b/app/assets/javascripts/pipelines/components/nav_controls.js
index 6aa10531034..6aa10531034 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js
+++ b/app/assets/javascripts/pipelines/components/nav_controls.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/pipelines/components/navigation_tabs.js
index 1626ae17a30..1626ae17a30 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
+++ b/app/assets/javascripts/pipelines/components/navigation_tabs.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js b/app/assets/javascripts/pipelines/components/pipeline_url.js
index 4e183d5c8ec..4e183d5c8ec 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/pipelines/components/pipelines_actions.js
index 12d80768646..ffda18d2e0f 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.js
@@ -28,6 +28,8 @@ export default {
onClickAction(endpoint) {
this.isLoading = true;
+ $(this.$refs.tooltip).tooltip('destroy');
+
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
@@ -57,6 +59,7 @@ export default {
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
+ ref="tooltip"
:disabled="isLoading">
${playIconSvg}
<i
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/pipelines/components/pipelines_artifacts.js
index f18e2dfadaf..f18e2dfadaf 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/pipelines/components/stage.js
index a2c29002707..b8cc3630611 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/stage.js
+++ b/app/assets/javascripts/pipelines/components/stage.js
@@ -1,32 +1,11 @@
/* global Flash */
-import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
-import createdSvg from 'icons/_icon_status_created_borderless.svg';
-import failedSvg from 'icons/_icon_status_failed_borderless.svg';
-import manualSvg from 'icons/_icon_status_manual_borderless.svg';
-import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
-import runningSvg from 'icons/_icon_status_running_borderless.svg';
-import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
-import successSvg from 'icons/_icon_status_success_borderless.svg';
-import warningSvg from 'icons/_icon_status_warning_borderless.svg';
+import StatusIconEntityMap from '../../ci_status_icons';
export default {
data() {
- const svgsDictionary = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
- };
-
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
- svg: svgsDictionary[this.stage.status.icon],
};
},
@@ -89,6 +68,9 @@ export default {
triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
},
+ svgHTML() {
+ return StatusIconEntityMap[this.stage.status.icon];
+ },
},
template: `
<div>
@@ -100,7 +82,7 @@ export default {
data-toggle="dropdown"
type="button"
:aria-label="stage.title">
- <span v-html="svg" aria-hidden="true"></span>
+ <span v-html="svgHTML" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
diff --git a/app/assets/javascripts/vue_pipelines_index/components/status.js b/app/assets/javascripts/pipelines/components/status.js
index 21a281af438..21a281af438 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/status.js
+++ b/app/assets/javascripts/pipelines/components/status.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js b/app/assets/javascripts/pipelines/components/time_ago.js
index 498d0715f54..498d0715f54 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
+++ b/app/assets/javascripts/pipelines/components/time_ago.js
diff --git a/app/assets/javascripts/vue_pipelines_index/event_hub.js b/app/assets/javascripts/pipelines/event_hub.js
index 0948c2e5352..0948c2e5352 100644
--- a/app/assets/javascripts/vue_pipelines_index/event_hub.js
+++ b/app/assets/javascripts/pipelines/event_hub.js
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/pipelines/index.js
index 48f9181a8d9..48f9181a8d9 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js
+++ b/app/assets/javascripts/pipelines/index.js
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js
index 5575aa72d5e..6eea4812f33 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js
+++ b/app/assets/javascripts/pipelines/pipelines.js
@@ -4,8 +4,8 @@ import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
import TablePaginationComponent from '../vue_shared/components/table_pagination';
-import EmptyState from './components/empty_state';
-import ErrorState from './components/error_state';
+import EmptyState from './components/empty_state.vue';
+import ErrorState from './components/error_state.vue';
import NavigationTabs from './components/navigation_tabs';
import NavigationControls from './components/nav_controls';
import Poll from '../lib/utils/poll';
diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 255cd513490..255cd513490 100644
--- a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
diff --git a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js
index 377ec8ba2cc..377ec8ba2cc 100644
--- a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
+++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 5b6bb2bf3f5..85659d7fa39 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -57,8 +57,11 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode
- if ($(e.target).hasClass('js-note-text')) {
- $('.js-md-preview-button').focus();
+ const $target = $(e.target);
+ const $form = $target.closest('form');
+
+ if ($target.hasClass('js-note-text')) {
+ $('.js-md-preview-button', $form).focus();
}
return $(document).triggerHandler('markdown-preview:toggle', [e]);
};
diff --git a/app/assets/javascripts/shortcuts_wiki.js b/app/assets/javascripts/shortcuts_wiki.js
new file mode 100644
index 00000000000..8a075062a48
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_wiki.js
@@ -0,0 +1,16 @@
+/* eslint-disable class-methods-use-this */
+/* global Mousetrap */
+/* global ShortcutsNavigation */
+
+import findAndFollowLink from './shortcuts_dashboard_navigation';
+
+export default class ShortcutsWiki extends ShortcutsNavigation {
+ constructor() {
+ super();
+ Mousetrap.bind('e', this.editWiki);
+ }
+
+ editWiki() {
+ findAndFollowLink('.js-wiki-edit');
+ }
+}
diff --git a/app/assets/javascripts/usage_ping.js b/app/assets/javascripts/usage_ping.js
new file mode 100644
index 00000000000..fd3af7d7ab6
--- /dev/null
+++ b/app/assets/javascripts/usage_ping.js
@@ -0,0 +1,15 @@
+function UsagePing() {
+ const usageDataUrl = $('.usage-data').data('endpoint');
+
+ $.ajax({
+ type: 'GET',
+ url: usageDataUrl,
+ dataType: 'html',
+ success(html) {
+ $('.usage-data').html(html);
+ },
+ });
+}
+
+window.gl = window.gl || {};
+window.gl.UsagePing = UsagePing;
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
index fa078b48bf8..b9d57cbcad4 100644
--- a/app/assets/javascripts/user_callout.js
+++ b/app/assets/javascripts/user_callout.js
@@ -18,7 +18,7 @@ export default class UserCallout {
dismissCallout(e) {
const $currentTarget = $(e.currentTarget);
- Cookies.set(USER_CALLOUT_COOKIE, 'true');
+ Cookies.set(USER_CALLOUT_COOKIE, 'true', { expires: 365 });
if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove();
diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js
index 5db0d936ad8..ce7eb76dc71 100644
--- a/app/assets/javascripts/user_tabs.js
+++ b/app/assets/javascripts/user_tabs.js
@@ -94,15 +94,17 @@ content on the Users#show page.
e.preventDefault();
$('.tab-pane.active').empty();
- this.loadTab($(e.target).attr('href'), this.getCurrentAction());
+ const endpoint = $(e.target).attr('href');
+ this.loadTab(this.getCurrentAction(), endpoint);
}
tabShown(event) {
const $target = $(event.target);
const action = $target.data('action');
const source = $target.attr('href');
- this.setTab(source, action);
- return this.setCurrentAction(source, action);
+ const endpoint = $target.data('endpoint');
+ this.setTab(action, endpoint);
+ return this.setCurrentAction(source);
}
activateTab(action) {
@@ -110,27 +112,27 @@ content on the Users#show page.
.tab('show');
}
- setTab(source, action) {
+ setTab(action, endpoint) {
if (this.loaded[action]) {
return;
}
if (action === 'activity') {
- this.loadActivities(source);
+ this.loadActivities();
}
const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
if (loadableActions.indexOf(action) > -1) {
- return this.loadTab(source, action);
+ return this.loadTab(action, endpoint);
}
}
- loadTab(source, action) {
+ loadTab(action, endpoint) {
return $.ajax({
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
- url: source,
+ url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
@@ -140,7 +142,7 @@ content on the Users#show page.
});
}
- loadActivities(source) {
+ loadActivities() {
if (this.loaded['activity']) {
return;
}
@@ -155,7 +157,7 @@ content on the Users#show page.
.toggle(status);
}
- setCurrentAction(source, action) {
+ setCurrentAction(source) {
let new_state = source;
new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 3325a7d429c..30902767705 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -56,6 +56,9 @@
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
$loading.fadeOut();
+ })
+ .catch(function () {
+ $loading.fadeOut();
});
};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/empty_state.js b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js
deleted file mode 100644
index 56b4858f4b4..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/components/empty_state.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg';
-
-export default {
- props: {
- helpPagePath: {
- type: String,
- required: true,
- },
- },
-
- template: `
- <div class="row empty-state">
- <div class="col-xs-12">
- <div class="svg-content">
- ${pipelinesEmptyStateSVG}
- </div>
- </div>
-
- <div class="col-xs-12 text-center">
- <div class="text-content">
- <h4>Build with confidence</h4>
- <p>
- Continous Integration can help catch bugs by running your tests automatically,
- while Continuous Deployment can help you deliver code to your product environment.
- </p>
- <a :href="helpPagePath" class="btn btn-info">
- Get started with Pipelines
- </a>
- </div>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/error_state.js b/app/assets/javascripts/vue_pipelines_index/components/error_state.js
deleted file mode 100644
index e5d228bddf8..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/components/error_state.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg';
-
-export default {
- template: `
- <div class="row empty-state js-pipelines-error-state">
- <div class="col-xs-12">
- <div class="svg-content">
- ${pipelinesErrorStateSVG}
- </div>
- </div>
-
- <div class="col-xs-12 text-center">
- <div class="text-content">
- <h4>The API failed to fetch the pipelines.</h4>
- </div>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index f5b3cb9214e..62b7131de51 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -1,12 +1,12 @@
/* eslint-disable no-param-reassign */
-import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
-import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
-import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
-import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
-import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
-import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
-import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
+import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
+import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
+import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
+import PipelinesStatusComponent from '../../pipelines/components/status';
+import PipelinesStageComponent from '../../pipelines/components/stage';
+import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
+import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
import CommitComponent from './commit';
/**
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 90935b9616b..7c50b80fd2b 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -145,3 +145,17 @@ a {
.dropdown-menu-nav a {
transition: none;
}
+
+@keyframes fadeIn {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
+
+.fade-in {
+ animation: fadeIn $fade-in-duration 1;
+}
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index b849cc2d853..f614f262316 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -38,6 +38,15 @@
height: 300px;
overflow-y: scroll;
}
+
+ .disabled {
+ cursor: default;
+ opacity: 0.5;
+
+ &:hover {
+ transform: none;
+ }
+ }
}
.emoji-search {
@@ -154,6 +163,17 @@
}
}
+ &.user-authored {
+ cursor: default;
+ opacity: 0.65;
+
+ &:hover,
+ &:active {
+ background-color: $white-light;
+ border-color: $border-color;
+ }
+ }
+
&.btn {
&:focus {
outline: 0;
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 52425262925..f3e2a5db0a6 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -230,7 +230,6 @@
float: right;
margin-top: 8px;
padding-bottom: 8px;
- border-bottom: 1px solid $border-color;
}
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 9a0f7a14e57..759401a7806 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -5,7 +5,7 @@
direction: rtl;
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
- overflow-x: scroll;
+ overflow-x: auto;
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 2c33b235980..0fd7203e72b 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -40,6 +40,10 @@
line-height: 24px;
}
+.bold {
+ font-weight: 600;
+}
+
.tab-content {
overflow: visible;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 7767826b033..30d785464ac 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -14,14 +14,32 @@
}
}
+@mixin set-visible {
+ transform: translateY(0);
+ visibility: visible;
+ opacity: 1;
+ transition-duration: 100ms, 150ms, 25ms;
+ transition-delay: 35ms, 50ms, 25ms;
+}
+
+@mixin set-invisible {
+ transform: translateY(-10px);
+ visibility: hidden;
+ opacity: 0;
+ transition-property: opacity, transform, visibility;
+ transition-duration: 70ms, 250ms, 250ms;
+ transition-timing-function: linear, $dropdown-animation-timing;
+ transition-delay: 25ms, 50ms, 0ms;
+}
+
.open {
.dropdown-menu,
.dropdown-menu-nav {
display: block;
+ @include set-visible;
@media (max-width: $screen-xs-max) {
width: 100%;
- min-width: 240px;
}
}
@@ -161,8 +179,9 @@
.dropdown-menu,
.dropdown-menu-nav {
- display: none;
+ display: block;
position: absolute;
+ width: 100%;
top: 100%;
left: 0;
z-index: 9;
@@ -176,6 +195,12 @@
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
+ overflow: hidden;
+ @include set-invisible;
+
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
&.is-loading {
.dropdown-content {
@@ -252,6 +277,23 @@
}
}
+.filtered-search-box-input-container .dropdown-menu,
+.filtered-search-box-input-container .dropdown-menu-nav,
+.comment-type-dropdown .dropdown-menu {
+ display: none;
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+
+.filtered-search-box-input-container {
+ .dropdown-menu,
+ .dropdown-menu-nav {
+ max-width: 280px;
+ width: auto;
+ }
+}
+
.dropdown-menu-drop-up {
top: auto;
bottom: 100%;
@@ -326,6 +368,10 @@
.dropdown-select {
width: $dropdown-width;
+
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
}
.dropdown-menu-align-right {
@@ -564,3 +610,28 @@
color: $gl-text-color-secondary;
}
}
+
+.droplab-item-ignore {
+ pointer-events: none;
+}
+
+.pika-single.animate-picker.is-bound,
+.pika-single.animate-picker.is-bound.is-hidden {
+ /*
+ * Having `!important` is not recommended but
+ * since `pikaday` sets positioning inline
+ * there's no way it can be gracefully overridden
+ * using config options.
+ */
+ position: absolute !important;
+ display: block;
+}
+
+.pika-single.animate-picker.is-bound {
+ @include set-visible;
+}
+
+.pika-single.animate-picker.is-bound.is-hidden {
+ @include set-invisible;
+ overflow: hidden;
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 12465d4a70b..11d44df4867 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -82,7 +82,7 @@
.input-token:last-child {
flex: 1;
-webkit-flex: 1;
- max-width: initial;
+ max-width: inherit;
}
}
@@ -246,17 +246,17 @@
}
}
-.filtered-search-history-dropdown-toggle-button {
+.filtered-search-history-dropdown-wrapper {
+ position: static;
display: flex;
- align-items: center;
+ flex-direction: column;
+}
+
+.filtered-search-history-dropdown-toggle-button {
+ flex: 1;
width: auto;
- height: 100%;
- padding-top: 0;
- padding-left: 0.75em;
- padding-bottom: 0;
- padding-right: 0.5em;
+ padding-right: 10px;
- background-color: transparent;
border-radius: 0;
border-top: 0;
border-left: 0;
@@ -264,6 +264,7 @@
border-right: 1px solid $border-color;
color: $gl-text-color-secondary;
+ line-height: 1;
transition: color 0.1s linear;
@@ -275,24 +276,21 @@
}
.dropdown-toggle-text {
+ display: inline-block;
color: inherit;
.fa {
+ vertical-align: middle;
color: inherit;
}
}
.fa {
- position: initial;
+ position: static;
}
}
-.filtered-search-history-dropdown-wrapper {
- position: initial;
- flex-shrink: 0;
-}
-
.filtered-search-history-dropdown {
width: 40%;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index abb092623c0..6d9218310eb 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -155,7 +155,7 @@ header {
.header-logo {
display: inline-block;
- margin: 0 7px 0 2px;
+ margin: 0 12px 0 2px;
position: relative;
top: 10px;
transition-duration: .3s;
@@ -186,7 +186,7 @@ header {
display: flex;
align-items: flex-start;
flex: 1 1 auto;
- padding-top: (($header-height - 19) / 2);
+ padding-top: 14px;
overflow: hidden;
}
@@ -329,8 +329,17 @@ header {
.header-user {
.dropdown-menu-nav {
+ width: auto;
min-width: 140px;
margin-top: -5px;
+
+ .current-user {
+ padding: 5px 18px;
+
+ .user-name {
+ display: block;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index e6d808717f3..b6cf5101d60 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -110,7 +110,7 @@
.top-area {
@include clearfix;
- border-bottom: 1px solid $white-normal;
+ border-bottom: 1px solid $border-color;
.nav-text {
padding-top: 16px;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index c241816788b..1839cadcc10 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -158,6 +158,7 @@
li.task-list-item {
list-style-type: none;
position: relative;
+ min-height: 22px;
padding-left: 28px;
margin-left: 0 !important;
@@ -337,3 +338,32 @@ h4 {
.idiff.addition {
background: $line-added-dark;
}
+
+
+/**
+ * form text input i.e. search bar, comments, forms, etc.
+ */
+input,
+textarea {
+ &::-webkit-input-placeholder {
+ color: $placeholder-text-color;
+ }
+
+ // support firefox 19+ vendor prefix
+ &::-moz-placeholder {
+ color: $placeholder-text-color;
+ opacity: 1; // FF defaults to 0.54
+ }
+
+ // scss-lint:disable PseudoElement
+ // support Edge vendor prefix
+ &::-ms-input-placeholder {
+ color: $placeholder-text-color;
+ }
+
+ // scss-lint:disable PseudoElement
+ // support IE vendor prefix
+ &:-ms-input-placeholder {
+ color: $placeholder-text-color;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 712eb7caf33..49741c963df 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -26,6 +26,7 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
$gray-darkest: #c4c4c4;
+$green-25: #f6fcf8;
$green-50: #e4f5eb;
$green-100: #bae6cc;
$green-200: #8dd5aa;
@@ -37,6 +38,7 @@ $green-700: #12753a;
$green-800: #0e5a2d;
$green-900: #0a4020;
+$blue-25: #f6fafd;
$blue-50: #e4eff9;
$blue-100: #bcd7f1;
$blue-200: #8fbce8;
@@ -48,6 +50,7 @@ $blue-700: #17599c;
$blue-800: #134a81;
$blue-900: #0f3b66;
+$orange-25: #fffcf8;
$orange-50: #fff2e1;
$orange-100: #fedfb3;
$orange-200: #feca81;
@@ -59,6 +62,7 @@ $orange-700: #c26700;
$orange-800: #a35100;
$orange-900: #853b00;
+$red-25: #fef7f6;
$red-50: #fbe7e4;
$red-100: #f4c4bc;
$red-200: #ed9d90;
@@ -107,6 +111,7 @@ $gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-header-color: #4c4e54;
$gl-header-nav-hover-color: #434343;
+$placeholder-text-color: rgba(0, 0, 0, .42);
/*
* Lists
@@ -147,7 +152,7 @@ $gl-sidebar-padding: 22px;
/*
* Misc
*/
-$row-hover: lighten($blue-50, 2%);
+$row-hover: $blue-25;
$row-hover-border: $blue-100;
$progress-color: #c0392b;
$header-height: 50px;
@@ -223,18 +228,18 @@ $gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background;
/*
* Commit Diff Colors
*/
-$added: $green-300;
-$deleted: $red-300;
-$line-added: $green-50;
-$line-added-dark: $green-100;
-$line-removed: $red-50;
-$line-removed-dark: $red-100;
-$line-number-old: lighten($red-100, 5%);
-$line-number-new: lighten($green-100, 5%);
-$line-number-select: lighten($orange-100, 5%);
-$line-target-blue: $blue-50;
-$line-select-yellow: $orange-50;
-$line-select-yellow-dark: $orange-100;
+$added: #63c363;
+$deleted: #f77;
+$line-added: #ecfdf0;
+$line-added-dark: #c7f0d2;
+$line-removed: #fbe9eb;
+$line-removed-dark: #fac5cd;
+$line-number-old: #f9d7dc;
+$line-number-new: #ddfbe6;
+$line-number-select: #fbf2da;
+$line-target-blue: #f6faff;
+$line-select-yellow: #fcf8e7;
+$line-select-yellow-dark: #f0e2bd;
$dark-diff-match-bg: rgba(255, 255, 255, 0.3);
$dark-diff-match-color: rgba(255, 255, 255, 0.1);
$file-mode-changed: #777;
@@ -454,6 +459,11 @@ $label-remove-border: rgba(0, 0, 0, .1);
$label-border-radius: 100px;
/*
+* Animation
+*/
+$fade-in-duration: 200ms;
+
+/*
* Lint
*/
$lint-incorrect-color: $red-500;
@@ -552,3 +562,8 @@ $filter-name-text-color: rgba(0, 0, 0, 0.55);
$filter-value-text-color: rgba(0, 0, 0, 0.85);
$filter-name-selected-color: #ebebeb;
$filter-value-selected-color: #d7d7d7;
+
+/*
+Animation Functions
+*/
+$dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1);
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 03fddaeb163..411f1c4442b 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -39,7 +39,7 @@
overflow-y: hidden;
font-size: 12px;
- .fa-refresh {
+ .fa-spinner {
font-size: 24px;
margin-left: 20px;
}
@@ -61,8 +61,9 @@
.truncated-info {
text-align: center;
border-bottom: 1px solid;
- background-color: $black-transparent;
+ background-color: $black;
height: 45px;
+ padding: 15px;
&.affix {
top: 0;
@@ -87,6 +88,16 @@
right: 5px;
left: 5px;
}
+
+ .truncated-info-size {
+ margin: 0 5px;
+ }
+
+ .raw-link {
+ color: inherit;
+ margin-left: 5px;
+ text-decoration: underline;
+ }
}
}
@@ -219,7 +230,7 @@
font-size: 12px;
position: relative;
- .fa-refresh {
+ .fa-spinner {
font-size: 24px;
}
@@ -366,7 +377,7 @@
background-color: $row-hover;
}
- .fa-refresh {
+ .fa-spinner {
font-size: 13px;
margin-left: 3px;
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 0dad91ba128..9e3142c8aa3 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -135,7 +135,7 @@
.text-expander {
display: inline-block;
- background: $gray-light;
+ background: $white-light;
color: $gl-text-color-secondary;
padding: 0 5px;
cursor: pointer;
@@ -146,6 +146,11 @@
line-height: $gl-font-size;
outline: none;
+ &.open {
+ background: $gray-light;
+ box-shadow: inset 0 0 2px rgba($black, 0.2);
+ }
+
&:hover {
background-color: darken($gray-light, 10%);
text-decoration: none;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 1aa1079903c..1b4694377b3 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -106,6 +106,10 @@
span {
white-space: pre-wrap;
}
+
+ .line {
+ word-wrap: break-word;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 79da490675a..5b723f7c722 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -10,10 +10,14 @@
position: relative;
&.event-inline {
- .profile-icon {
+ .system-note-image {
top: 20px;
}
+ .user-avatar {
+ top: 14px;
+ }
+
.event-title,
.event-item-timestamp {
line-height: 40px;
@@ -24,7 +28,7 @@
color: $gl-text-color;
}
- .profile-icon {
+ .system-note-image {
position: absolute;
left: 0;
top: 14px;
@@ -35,15 +39,18 @@
fill: $gl-text-color-secondary;
}
- &.open-icon svg {
- fill: $green-300;
+ &.opened-icon,
+ &.created-icon {
+ svg {
+ fill: $green-300;
+ }
}
&.closed-icon svg {
fill: $red-300;
}
- &.fork-icon svg {
+ &.accepted-icon svg {
fill: $blue-300;
}
}
@@ -128,8 +135,7 @@
li {
&.commit {
background: transparent;
- padding: 3px;
- padding-left: 0;
+ padding: 0;
border: none;
.commit-row-title {
@@ -183,7 +189,7 @@
max-width: 100%;
}
- .profile-icon {
+ .system-note-image {
display: none;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 0bca3e93e4c..8d3d1a72b9b 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -210,10 +210,6 @@
}
}
- .bold {
- font-weight: 600;
- }
-
.light {
font-weight: normal;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 2f946ab2f59..6a419384a34 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -523,11 +523,12 @@
}
.content-block {
- border-top: 1px solid $border-color;
padding: $gl-padding-top $gl-padding;
}
.comments-disabled-notif {
+ line-height: 28px;
+
.btn {
margin-left: 5px;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 94ea4c5c8c6..2ea2ff8362b 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -18,12 +18,12 @@ ul.notes {
float: left;
svg {
- width: 18px;
- height: 18px;
+ width: 16px;
+ height: 16px;
fill: $gray-darkest;
position: absolute;
- left: 30px;
- top: 15px;
+ left: 0;
+ top: 16px;
}
}
@@ -123,6 +123,9 @@ ul.notes {
}
.note-emoji-button {
+ position: relative;
+ line-height: 1;
+
.fa-spinner {
display: none;
}
@@ -144,6 +147,10 @@ ul.notes {
padding: 0;
clear: both;
+ @media (min-width: $screen-sm-min) {
+ margin-left: 65px;
+ }
+
&.timeline-entry::after {
clear: none;
}
@@ -172,6 +179,10 @@ ul.notes {
.timeline-content {
padding: 14px 10px;
+
+ @media (min-width: $screen-sm-min) {
+ margin-left: 20px;
+ }
}
.note-header {
@@ -344,6 +355,15 @@ ul.notes {
font-size: 14px;
}
+.note-header {
+ display: flex;
+ justify-content: space-between;
+}
+
+.note-header-info {
+ min-width: 0;
+}
+
.note-headline-light {
display: inline;
@@ -363,21 +383,27 @@ ul.notes {
}
}
+.note-headline-meta {
+ display: inline-block;
+ white-space: nowrap;
+}
+
/**
* Actions for Discussions/Notes
*/
-.discussion-actions,
-.note-actions {
+.discussion-actions {
float: right;
margin-left: 10px;
color: $gray-darkest;
}
.note-actions {
- position: absolute;
- right: 0;
- top: 0;
+ flex-shrink: 0;
+ // For PhantomJS that does not support flex
+ float: right;
+ margin-left: 10px;
+ color: $gray-darkest;
.note-action-button {
margin-left: 8px;
@@ -420,7 +446,8 @@ ul.notes {
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
- margin-left: -20px;
+ top: 0;
+ left: 0;
opacity: 0;
}
@@ -619,7 +646,6 @@ ul.notes {
}
&:not(.is-disabled):hover,
- &:not(.is-disabled):focus,
&.is-active {
color: $gl-text-green;
@@ -633,6 +659,11 @@ ul.notes {
height: 15px;
width: 15px;
}
+
+ .loading {
+ margin: 0;
+ height: auto;
+ }
}
.discussion-next-btn {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 8c6dd392865..fe084eb9397 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -289,8 +289,12 @@ table.u2f-registrations {
margin: 0 auto;
.bordered-box {
- border: 1px solid $border-color;
+ border: 1px solid $blue-300;
border-radius: $border-radius-default;
+ background-color: $blue-25;
+ position: relative;
+ display: flex;
+ justify-content: center;
}
.landing {
@@ -298,28 +302,59 @@ table.u2f-registrations {
margin-bottom: $gl-padding;
.close {
- margin-right: 20px;
- }
+ position: absolute;
+ right: 20px;
+ opacity: 1;
+
+ .dismiss-icon {
+ float: right;
+ cursor: pointer;
+ color: $blue-300;
+ }
- .dismiss-icon {
- float: right;
- cursor: pointer;
- color: $cycle-analytics-dismiss-icon-color;
+ &:hover {
+ background-color: transparent;
+ border: 0;
+
+ .dismiss-icon {
+ color: $blue-400;
+ }
+ }
}
.svg-container {
- text-align: center;
+ margin-right: 30px;
+ display: inline-block;
svg {
- width: 136px;
- height: 136px;
+ height: 110px;
+ vertical-align: top;
}
}
+
+ .user-callout-copy {
+ display: inline-block;
+ vertical-align: top;
+ }
}
@media(max-width: $screen-xs-max) {
- .inner-content {
- padding-left: 30px;
+ text-align: center;
+
+ .bordered-box {
+ display: block;
+ }
+
+ .landing {
+ .svg-container,
+ .user-callout-copy {
+ margin: 0;
+ display: block;
+
+ svg {
+ height: 75px;
+ }
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 4ca5683b628..3f8995e0231 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -600,6 +600,10 @@ pre.light-well {
.avatar-container {
align-self: flex-start;
+
+ > a {
+ width: 100%;
+ }
}
.project-details {
@@ -933,27 +937,23 @@ pre.light-well {
}
.variable-key {
- width: 300px;
- max-width: 300px;
+ max-width: 120px;
overflow: hidden;
word-wrap: break-word;
-
- // override bootstrap
- white-space: normal!important;
-
- @media (max-width: $screen-sm-max) {
- width: 150px;
- max-width: 150px;
- }
+ white-space: nowrap;
+ text-overflow: ellipsis;
}
.variable-value {
- @media(max-width: $screen-xs-max) {
- width: 150px;
- max-width: 150px;
- overflow: hidden;
- word-wrap: break-word;
- }
+ max-width: 150px;
+ overflow: hidden;
+ word-wrap: break-word;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .variable-menu {
+ text-align: right;
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 543d2ece3df..b9818ffcf42 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -124,7 +124,13 @@ input[type="checkbox"]:hover {
// Custom dropdown positioning
.dropdown-menu {
- top: 37px;
+ transition-property: opacity, transform;
+ transition-duration: 250ms, 250ms;
+ transition-delay: 0ms, 25ms;
+ transition-timing-function: $dropdown-animation-timing;
+ transform: translateY(0);
+ opacity: 0;
+ display: block;
left: -5px;
padding: 0;
@@ -156,6 +162,13 @@ input[type="checkbox"]:hover {
color: $layout-link-gray;
}
}
+
+ .dropdown-menu {
+ transition-duration: 100ms, 75ms;
+ transition-delay: 75ms, 100ms;
+ transform: translateY(13px);
+ opacity: 1;
+ }
}
&.has-value {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 515d8e1523b..643993d035e 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -17,6 +17,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
end
+ def usage_data
+ respond_to do |format|
+ format.html do
+ usage_data = Gitlab::UsageData.data
+ usage_data_json = params[:pretty] ? JSON.pretty_generate(usage_data) : usage_data.to_json
+
+ render html: Gitlab::Highlight.highlight('payload.json', usage_data_json)
+ end
+ format.json { render json: Gitlab::UsageData.to_json }
+ end
+ end
+
def reset_runners_token
@application_setting.reset_runners_registration_token!
flash[:notice] = 'New runners registration token has been generated!'
@@ -135,6 +147,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:version_check_enabled,
:terminal_max_session_time,
:polling_interval_multiplier,
+ :usage_ping_enabled,
disabled_oauth_sign_in_sources: [],
import_sources: [],
diff --git a/app/controllers/admin/cohorts_controller.rb b/app/controllers/admin/cohorts_controller.rb
new file mode 100644
index 00000000000..9b77c554908
--- /dev/null
+++ b/app/controllers/admin/cohorts_controller.rb
@@ -0,0 +1,11 @@
+class Admin::CohortsController < Admin::ApplicationController
+ def index
+ if current_application_settings.usage_ping_enabled
+ cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
+ CohortsService.new.execute
+ end
+
+ @cohorts = CohortsSerializer.new.represent(cohorts_results)
+ end
+ end
+end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index f28bbdeff5a..fc8d4d02ddf 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -43,9 +43,13 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user)
+ status = Members::CreateService.new(@group, current_user, params).execute
- redirect_to [:admin, @group], notice: 'Users were successfully added.'
+ if status
+ redirect_to [:admin, @group], notice: 'Users were successfully added.'
+ else
+ redirect_to [:admin, @group], alert: 'No users specified.'
+ end
end
def destroy
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index 2abfa22712d..1d66955bb71 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -7,7 +7,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
spam_log = SpamLog.find(params[:id])
if params[:remove_user]
- spam_log.remove_user
+ spam_log.remove_user(deleted_by: current_user)
redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed."
else
spam_log.destroy
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 9ac8197e45a..183eb00ef67 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -1,17 +1,29 @@
module CreatesCommit
extend ActiveSupport::Concern
+ def set_start_branch_to_branch_name
+ branch_exists = @repository.find_branch(@branch_name)
+ @start_branch = @branch_name if branch_exists
+ end
+
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
- set_commit_variables
+ if can?(current_user, :push_code, @project)
+ @project_to_commit_into = @project
+ @branch_name ||= @ref
+ else
+ @project_to_commit_into = current_user.fork_of(@project)
+ @branch_name ||= @project_to_commit_into.repository.next_branch('patch')
+ end
+
+ @start_branch ||= @ref || @branch_name
commit_params = @commit_params.merge(
- start_project: @mr_target_project,
- start_branch: @mr_target_branch,
- target_branch: @mr_source_branch
+ start_project: @project,
+ start_branch: @start_branch,
+ branch_name: @branch_name
)
- result = service.new(
- @mr_source_project, current_user, commit_params).execute
+ result = service.new(@project_to_commit_into, current_user, commit_params).execute
if result[:status] == :success
update_flash_notice(success_notice)
@@ -72,30 +84,30 @@ module CreatesCommit
def new_merge_request_path
new_namespace_project_merge_request_path(
- @mr_source_project.namespace,
- @mr_source_project,
+ @project_to_commit_into.namespace,
+ @project_to_commit_into,
merge_request: {
- source_project_id: @mr_source_project.id,
- target_project_id: @mr_target_project.id,
- source_branch: @mr_source_branch,
- target_branch: @mr_target_branch
+ source_project_id: @project_to_commit_into.id,
+ target_project_id: @project.id,
+ source_branch: @branch_name,
+ target_branch: @start_branch
}
)
end
def existing_merge_request_path
- namespace_project_merge_request_path(@mr_target_project.namespace, @mr_target_project, @merge_request)
+ namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
end
def merge_request_exists?
return @merge_request if defined?(@merge_request)
- @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened.
- find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch, source_project_id: @mr_source_project)
+ @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.
+ find_by(source_project_id: @project_to_commit_into, source_branch: @branch_name, target_branch: @start_branch)
end
def different_project?
- @mr_source_project != @mr_target_project
+ @project_to_commit_into != @project
end
def create_merge_request?
@@ -103,22 +115,6 @@ module CreatesCommit
# as the target branch in the same project,
# we don't want to create a merge request.
params[:create_merge_request].present? &&
- (different_project? || @mr_target_branch != @mr_source_branch)
- end
-
- def set_commit_variables
- if can?(current_user, :push_code, @project)
- @mr_source_project = @project
- @target_branch ||= @ref
- else
- @mr_source_project = current_user.fork_of(@project)
- @target_branch ||= @mr_source_project.repository.next_branch('patch')
- end
-
- # Merge request to this project
- @mr_target_project = @project
- @mr_target_branch ||= @ref || @target_branch
-
- @mr_source_branch = @target_branch
+ (different_project? || @start_branch != @branch_name)
end
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index c13333641d3..b1bacc8ffe5 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -1,6 +1,32 @@
module MembershipActions
extend ActiveSupport::Concern
+ def create
+ status = Members::CreateService.new(membershipable, current_user, params).execute
+
+ redirect_url = members_page_url
+
+ if status
+ redirect_to redirect_url, notice: 'Users were successfully added.'
+ else
+ redirect_to redirect_url, alert: 'No users specified.'
+ end
+ end
+
+ def destroy
+ Members::DestroyService.new(membershipable, current_user, params).
+ execute(:all)
+
+ respond_to do |format|
+ format.html do
+ message = "User was successfully removed from #{source_type}."
+ redirect_to members_page_url, notice: message
+ end
+
+ format.js { head :ok }
+ end
+ end
+
def request_access
membershipable.request_access(current_user)
@@ -11,20 +37,20 @@ module MembershipActions
def approve_access_request
Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
- redirect_to polymorphic_url([membershipable, :members])
+ redirect_to members_page_url
end
def leave
member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
execute(:all)
- source_type = membershipable.class.to_s.humanize(capitalize: false)
notice =
if member.request?
"Your access request to the #{source_type} has been withdrawn."
else
"You left the \"#{membershipable.human_name}\" #{source_type}."
end
+
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice
@@ -35,4 +61,16 @@ module MembershipActions
def membershipable
raise NotImplementedError
end
+
+ def members_page_url
+ if membershipable.is_a?(Project)
+ project_settings_members_path(membershipable)
+ else
+ polymorphic_url([membershipable, :members])
+ end
+ end
+
+ def source_type
+ @source_type ||= membershipable.class.to_s.humanize(capitalize: false)
+ end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 00c50f9d0ad..8fc234a62b1 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -21,21 +21,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member = @group.group_members.new
end
- def create
- if params[:user_ids].blank?
- return redirect_to(group_group_members_path(@group), alert: 'No users specified.')
- end
-
- @group.add_users(
- params[:user_ids].split(','),
- params[:access_level],
- current_user: current_user,
- expires_at: params[:expires_at]
- )
-
- redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
- end
-
def update
@group_member = @group.group_members.find(params[:id])
@@ -44,15 +29,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member.update_attributes(member_params)
end
- def destroy
- Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
-
- respond_to do |format|
- format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
- format.js { head :ok }
- end
- end
-
def resend_invite
redirect_path = group_group_members_path(@group)
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index f1a93ccb3ad..e2f81b09adc 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -89,9 +89,4 @@ class Projects::ApplicationController < ApplicationController
def builds_enabled
return render_404 unless @project.feature_available?(:builds, current_user)
end
-
- def update_ref
- branch_exists = @repository.find_branch(@target_branch)
- @ref = @target_branch if branch_exists
- end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 73706bf8dae..9fce1db6742 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -25,10 +25,10 @@ class Projects::BlobController < Projects::ApplicationController
end
def create
- update_ref
+ set_start_branch_to_branch_name
create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
- success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) },
+ success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @file_path)) },
failure_view: :new,
failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
end
@@ -69,10 +69,10 @@ class Projects::BlobController < Projects::ApplicationController
end
def destroy
- create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.",
- success_path: -> { namespace_project_tree_path(@project.namespace, @project, @target_branch) },
- failure_view: :show,
- failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
+ create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.",
+ success_path: -> { namespace_project_tree_path(@project.namespace, @project, @branch_name) },
+ failure_view: :show,
+ failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end
def diff
@@ -127,16 +127,16 @@ class Projects::BlobController < Projects::ApplicationController
def after_edit_path
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid])
- if from_merge_request && @target_branch == @ref
+ if from_merge_request && @branch_name == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"##{hexdigest(@path)}"
else
- namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
+ namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @path))
end
end
def editor_variables
- @target_branch = params[:target_branch]
+ @branch_name = params[:branch_name]
@file_path =
if action_name.to_s == 'create'
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index d25bbddd1bb..2b5f0383ac1 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -56,9 +56,7 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @start_branch.blank?
- @target_branch = create_new_branch? ? @commit.revert_branch_name : @start_branch
-
- @mr_target_branch = @start_branch
+ @branch_name = create_new_branch? ? @commit.revert_branch_name : @start_branch
create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
success_path: -> { successful_change_path }, failure_path: failed_change_path)
@@ -69,9 +67,7 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @start_branch.blank?
- @target_branch = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
-
- @mr_target_branch = @start_branch
+ @branch_name = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.",
success_path: -> { successful_change_path }, failure_path: failed_change_path)
@@ -84,7 +80,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def successful_change_path
- referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch)
+ referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @branch_name)
end
def failed_change_path
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index c6651254d70..008d2f5815f 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -61,7 +61,6 @@ class Projects::CompareController < Projects::ApplicationController
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@diff_notes_disabled = true
- @grouped_diff_discussions = {}
end
end
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 37f6f637ff0..10adddb4636 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -5,6 +5,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
if upload_pack? && upload_pack_allowed?
+ log_user_activity
+
render_ok
elsif receive_pack? && receive_pack_allowed?
render_ok
@@ -106,4 +108,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
def access_klass
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
end
+
+ def log_user_activity
+ Users::ActivityService.new(user, 'pull').execute
+ end
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index b668a9331e7..1e41f980f31 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -10,7 +10,7 @@ class Projects::HooksController < Projects::ApplicationController
@hook = @project.hooks.new(hook_params)
@hook.save
- unless @hook.valid?
+ unless @hook.valid?
@hooks = @project.hooks.select(&:persisted?)
flash[:alert] = @hook.errors.full_messages.join.html_safe
end
@@ -49,7 +49,7 @@ class Projects::HooksController < Projects::ApplicationController
def hook_params
params.require(:hook).permit(
- :build_events,
+ :job_events,
:pipeline_events,
:enable_ssl_verification,
:issues_events,
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 5c1f7e69ee8..09dc8b38229 100755
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -16,7 +16,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_pipeline_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs]
- before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :apply_diff_view_cookie!, only: [:new_diffs]
@@ -39,7 +38,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@collection_type = "MergeRequest"
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
- @merge_requests = @merge_requests.includes(merge_request_diff: :merge_request)
+ @merge_requests = @merge_requests.preload(merge_request_diff: :merge_request)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
@@ -101,34 +100,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format|
format.html { define_discussion_vars }
format.json do
- @merge_request_diff =
- if params[:diff_id]
- @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
- else
- @merge_request.merge_request_diff
- end
-
- @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
- @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
-
- if params[:start_sha].present?
- @start_sha = params[:start_sha]
- @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
-
- unless @start_version
- @start_sha = @merge_request_diff.head_commit_sha
- @start_version = @merge_request_diff
- end
- end
+ define_diff_vars
+ define_diff_comment_vars
@environment = @merge_request.environments_for(current_user).last
- if @start_sha
- compared_diff_version
- else
- original_diff_version
- end
-
render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") }
end
end
@@ -140,16 +116,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diff_for_path
if params[:id]
merge_request
+ define_diff_vars
define_diff_comment_vars
else
build_merge_request
+ @diffs = @merge_request.diffs(diff_options)
@diff_notes_disabled = true
- @grouped_diff_discussions = {}
end
define_commit_vars
- render_diff_for_path(@merge_request.diffs(diff_options))
+ render_diff_for_path(@diffs)
end
def commits
@@ -586,15 +563,46 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
end
+ def define_diff_vars
+ @merge_request_diff =
+ if params[:diff_id]
+ @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
+ else
+ @merge_request.merge_request_diff
+ end
+
+ @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
+ @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
+
+ if params[:start_sha].present?
+ @start_sha = params[:start_sha]
+ @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
+
+ unless @start_version
+ @start_sha = @merge_request_diff.head_commit_sha
+ @start_version = @merge_request_diff
+ end
+ end
+
+ @diffs =
+ if @start_sha
+ @merge_request_diff.compare_with(@start_sha).diffs(diff_options)
+ else
+ @merge_request_diff.diffs(diff_options)
+ end
+ end
+
def define_diff_comment_vars
@new_diff_note_attrs = {
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
}
+ @diff_notes_disabled = !@merge_request_diff.latest? || @start_sha
+
@use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
- @grouped_diff_discussions = @merge_request.grouped_diff_discussions
+ @grouped_diff_discussions = @merge_request.grouped_diff_discussions(@merge_request_diff.diff_refs)
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes))
end
@@ -678,16 +686,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end
- def compared_diff_version
- @diff_notes_disabled = true
- @diffs = @merge_request_diff.compare_with(@start_sha).diffs(diff_options)
- end
-
- def original_diff_version
- @diff_notes_disabled = !@merge_request_diff.latest?
- @diffs = @merge_request_diff.diffs(diff_options)
- end
-
def close_merge_request_without_source_project
if !@merge_request.source_project && @merge_request.open?
@merge_request.close
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 6e158e685e9..d2d26738582 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -10,18 +10,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
end
- def create
- status = Members::CreateService.new(@project, current_user, params).execute
-
- redirect_url = namespace_project_settings_members_path(@project.namespace, @project)
-
- if status
- redirect_to redirect_url, notice: 'Users were successfully added.'
- else
- redirect_to redirect_url, alert: 'No users or groups specified.'
- end
- end
-
def update
@project_member = @project.project_members.find(params[:id])
@@ -30,18 +18,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_member.update_attributes(member_params)
end
- def destroy
- Members::DestroyService.new(@project, current_user, params).
- execute(:all)
-
- respond_to do |format|
- format.html do
- redirect_to namespace_project_settings_members_path(@project.namespace, @project)
- end
- format.js { head :ok }
- end
- end
-
def resend_invite
redirect_path = namespace_project_settings_members_path(@project.namespace, @project)
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 637b61504d8..5e2182c883e 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -34,16 +34,16 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir
return render_404 unless @commit_params.values.all?
- update_ref
+ set_start_branch_to_branch_name
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
- success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)),
+ success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@branch_name, @dir_name)),
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
end
private
def assign_dir_vars
- @target_branch = params[:target_branch]
+ @branch_name = params[:branch_name]
@dir_name = File.join(@path, params[:dir_name])
@commit_params = {
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 47f7e0b1b28..6807c37f972 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -345,7 +345,11 @@ class ProjectsController < Projects::ApplicationController
end
def project_view_files?
- current_user && current_user.project_view == 'files'
+ if current_user
+ current_user.project_view == 'files'
+ else
+ project_view_files_allowed?
+ end
end
# Override extract_ref from ExtractsPath, which returns the branch and file path
@@ -359,4 +363,8 @@ class ProjectsController < Projects::ApplicationController
def get_id
project.repository.root_ref
end
+
+ def project_view_files_allowed?
+ !project.empty_repo? && can?(current_user, :download_code, project)
+ end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 8109427a45f..3ca14dee33c 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -60,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def resource
- @resource ||= Users::CreateService.new(current_user, sign_up_params).build
+ @resource ||= Users::BuildService.new(current_user, sign_up_params).execute
end
def devise_mapping
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index d3091a4f8e9..8c6ba4915cd 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -35,6 +35,7 @@ class SessionsController < Devise::SessionsController
# hide the signed-in notification
flash[:notice] = nil
log_audit_event(current_user, with: authentication_method)
+ log_user_activity(current_user)
end
end
@@ -123,6 +124,10 @@ class SessionsController < Devise::SessionsController
for_authentication.security_event
end
+ def log_user_activity(user)
+ Users::ActivityService.new(user, 'login').execute
+ end
+
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index b0ac1623fbe..3736e1ffcbb 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -14,15 +14,6 @@ module BlobHelper
options[:link_opts])
end
- def fork_path(project = @project, ref = @ref, path = @path, options = {})
- continue_params = {
- to: edit_path,
- notice: edit_in_new_fork_notice,
- notice_now: edit_in_new_fork_notice_now
- }
- namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
- end
-
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil
@@ -34,10 +25,19 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
# This condition applies to anonymous or users who can edit directly
- elsif !current_user || (current_user && can_edit_blob?(blob, project, ref))
+ elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project)
- button_tag 'Edit', class: "#{common_classes} js-edit-blob-link-fork-toggler"
+ continue_params = {
+ to: edit_path,
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
+
+ button_tag 'Edit',
+ class: "#{common_classes} js-edit-blob-link-fork-toggler",
+ data: { action: 'edit', fork_path: fork_path }
end
end
@@ -48,21 +48,25 @@ module BlobHelper
return unless blob
+ common_classes = "btn btn-#{btn_class}"
+
if !on_top_of_branch?(project, ref)
- button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
+ button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer?
- button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
- elsif can_edit_blob?(blob, project, ref)
- button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
+ button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
+ elsif can_modify_blob?(blob, project, ref)
+ button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
continue_params = {
- to: request.fullpath,
+ to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to #{action} this file again.",
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
- link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post
+ button_tag label,
+ class: "#{common_classes} js-edit-blob-link-fork-toggler",
+ data: { action: action, fork_path: fork_path }
end
end
@@ -90,7 +94,7 @@ module BlobHelper
)
end
- def can_edit_blob?(blob, project = @project, ref = @ref)
+ def can_modify_blob?(blob, project = @project, ref = @ref)
!blob.lfs_pointer? && can_edit_tree?(project, ref)
end
@@ -118,6 +122,10 @@ module BlobHelper
blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw?
end
+ def blob_rendered_as_text?(blob)
+ blob_text_viewable?(blob) && blob.to_partial_path(@project) == 'text'
+ end
+
def blob_size(blob)
if blob.lfs_pointer?
blob.lfs_size
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 5e0886cc599..dc144906548 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -62,6 +62,8 @@ module DiffHelper
end
def parallel_diff_discussions(left, right, diff_file)
+ return unless @grouped_diff_discussions
+
discussions_left = discussions_right = nil
if left && (left.unchanged? || left.removed?)
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index fb872a13f74..5f5c76d3722 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -1,4 +1,15 @@
module EventsHelper
+ ICON_NAMES_BY_EVENT_TYPE = {
+ 'pushed to' => 'icon_commit',
+ 'pushed new' => 'icon_commit',
+ 'created' => 'icon_status_open',
+ 'opened' => 'icon_status_open',
+ 'closed' => 'icon_status_closed',
+ 'accepted' => 'icon_code_fork',
+ 'commented on' => 'icon_comment_o',
+ 'deleted' => 'icon_trash_o'
+ }.freeze
+
def link_to_author(event)
author = event.author
@@ -183,4 +194,21 @@ module EventsHelper
"event-inline"
end
end
+
+ def icon_for_event(note)
+ icon_name = ICON_NAMES_BY_EVENT_TYPE[note]
+ custom_icon(icon_name) if icon_name
+ end
+
+ def icon_for_profile_event(event)
+ if current_path?('users#show')
+ content_tag :div, class: "system-note-image #{event.action_name.parameterize}-icon" do
+ icon_for_event(event.action_name)
+ end
+ else
+ content_tag :div, class: 'system-note-image user-avatar' do
+ author_avatar(event, size: 32)
+ end
+ end
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index ec57fec4f99..0b13dbf5f8d 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -165,11 +165,8 @@ module IssuablesHelper
html.html_safe
end
- def cached_assigned_issuables_count(assignee, issuable_type, state)
- cache_key = hexdigest(['assigned_issuables_count', assignee.id, issuable_type, state].join('-'))
- Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
- assigned_issuables_count(assignee, issuable_type, state)
- end
+ def assigned_issuables_count(issuable_type)
+ current_user.public_send("assigned_open_#{issuable_type}_count")
end
def issuable_filter_params
@@ -192,10 +189,6 @@ module IssuablesHelper
private
- def assigned_issuables_count(assignee, issuable_type, state)
- assignee.public_send("assigned_#{issuable_type}").public_send(state).count
- end
-
def sidebar_gutter_collapsed?
cookies[:collapsed_gutter] == 'true'
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 6978b0c89fd..82288f1da35 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -110,6 +110,14 @@ module IssuesHelper
end
end
+ def award_user_authored_class(award)
+ if award == 'thumbsdown' || award == 'thumbsup'
+ 'user-authored js-user-authored'
+ else
+ ''
+ end
+ end
+
def awards_sort(awards)
awards.sort_by do |award, notes|
if award == "thumbsup"
diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb
index 68c09c922a6..d5e77c7e271 100644
--- a/app/helpers/javascript_helper.rb
+++ b/app/helpers/javascript_helper.rb
@@ -3,7 +3,8 @@ module JavascriptHelper
javascript_include_tag asset_path(js)
end
- def page_specific_javascript_bundle_tag(js)
- javascript_include_tag(*webpack_asset_paths(js))
+ # deprecated; use webpack_bundle_tag directly instead
+ def page_specific_javascript_bundle_tag(bundle)
+ webpack_bundle_tag(bundle)
end
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 5f3d89cf6cb..eab0738a368 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -61,12 +61,23 @@ module NotesHelper
end
def discussion_diff_path(discussion)
- return unless discussion.diff_discussion?
+ if discussion.for_merge_request? && discussion.diff_discussion?
+ if discussion.active?
+ # Without a diff ID, the link always points to the latest diff version
+ diff_id = nil
+ elsif merge_request_diff = discussion.latest_merge_request_diff
+ diff_id = merge_request_diff.id
+ else
+ # If the discussion is not active, and we cannot find the latest
+ # merge request diff for this discussion, we return no path at all.
+ return
+ end
- if discussion.for_merge_request? && discussion.active?
- diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code)
+ diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, diff_id: diff_id, anchor: discussion.line_code)
elsif discussion.for_commit?
- namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code)
+ anchor = discussion.line_code if discussion.diff_discussion?
+
+ namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: anchor)
end
end
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 243ef39ef61..de959f13713 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -63,6 +63,10 @@ module PreferencesHelper
end
def anonymous_project_view
- @project.empty_repo? || !can?(current_user, :download_code, @project) ? 'activity' : 'readme'
+ if !@project.empty_repo? && can?(current_user, :download_code, @project)
+ 'files'
+ else
+ 'activity'
+ end
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 6b9e4267281..5f97e6114ea 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -24,7 +24,7 @@ module ProjectsHelper
return "(deleted)" unless author
- author_html = ""
+ author_html = ""
# Build avatar image tag
author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar]
@@ -45,7 +45,7 @@ module ProjectsHelper
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
- link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe
+ link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' }).html_safe
end
end
@@ -272,14 +272,14 @@ module ProjectsHelper
end
end
- def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil)
+ def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil)
namespace_project_new_blob_path(
project.namespace,
project,
project.default_branch || 'master',
file_name: file_name,
commit_message: commit_message || "Add #{file_name.downcase}",
- target_branch: target_branch,
+ branch_name: branch_name,
context: context
)
end
@@ -430,13 +430,22 @@ module ProjectsHelper
end
def visibility_select_options(project, selected_level)
- levels_options_array = Gitlab::VisibilityLevel.values.map do |level|
- [
+ level_options = Gitlab::VisibilityLevel.values.each_with_object([]) do |level, level_options|
+ next if restricted_levels.include?(level)
+
+ level_options << [
visibility_level_label(level),
{ data: { description: visibility_level_description(level, project) } },
level
]
end
- options_for_select(levels_options_array, selected_level)
+
+ options_for_select(level_options, selected_level)
+ end
+
+ def restricted_levels
+ return [] if current_user.admin?
+
+ current_application_settings.restricted_visibility_levels || []
end
end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 8c02b4061ca..979264c9421 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -42,7 +42,7 @@ module SnippetsHelper
0,
lined_content.size,
surrounding_lines
- ) if line.include?(query)
+ ) if line.downcase.include?(query.downcase)
end
used_lines.uniq.sort
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 3a5d1b97c36..2fda98cae90 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -62,6 +62,14 @@ module SortingHelper
}
end
+ def branches_sort_options_hash
+ {
+ sort_value_name => sort_title_name,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_oldest_updated => sort_title_oldest_updated
+ }
+ end
+
def sort_title_priority
'Priority'
end
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index fb95f2b565e..a762b320d56 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -5,7 +5,7 @@ module SubmoduleHelper
def submodule_links(submodule_item, ref = nil, repository = @repository)
url = repository.submodule_url_for(ref, submodule_item.path)
- return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/
+ return url, nil unless url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace = $1
project = $2
@@ -37,14 +37,16 @@ module SubmoduleHelper
end
def self_url?(url, namespace, project)
- return true if url == [Gitlab.config.gitlab.url, '/', namespace, '/',
- project, '.git'].join('')
- url == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
+ url_no_dotgit = url.chomp('.git')
+ return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/',
+ project].join('')
+ url_with_dotgit = url_no_dotgit + '.git'
+ url_with_dotgit == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
end
def relative_self_url?(url)
# (./)?(../repo.git) || (./)?(../../project/repo.git) )
- url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/
+ url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*(\.git)?\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*(\.git)?\z/
end
def standard_links(host, namespace, project, commit)
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 4a76c679bad..f1dab60524e 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -35,7 +35,7 @@ module TreeHelper
end
def on_top_of_branch?(project = @project, ref = @ref)
- project.repository.branch_names.include?(ref)
+ project.repository.branch_exists?(ref)
end
def can_edit_tree?(project = nil, ref = nil)
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
new file mode 100644
index 00000000000..6bacda9fe75
--- /dev/null
+++ b/app/helpers/webpack_helper.rb
@@ -0,0 +1,30 @@
+require 'webpack/rails/manifest'
+
+module WebpackHelper
+ def webpack_bundle_tag(bundle)
+ javascript_include_tag(*gitlab_webpack_asset_paths(bundle))
+ end
+
+ # override webpack-rails gem helper until changes can make it upstream
+ def gitlab_webpack_asset_paths(source, extension: nil)
+ return "" unless source.present?
+
+ paths = Webpack::Rails::Manifest.asset_paths(source)
+ if extension
+ paths = paths.select { |p| p.ends_with? ".#{extension}" }
+ end
+
+ # include full webpack-dev-server url for rspec tests running locally
+ if Rails.env.test? && Rails.configuration.webpack.dev_server.enabled
+ host = Rails.configuration.webpack.dev_server.host
+ port = Rails.configuration.webpack.dev_server.port
+ protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http'
+
+ paths.map! do |p|
+ "#{protocol}://#{host}:#{port}#{p}"
+ end
+ end
+
+ paths
+ end
+end
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 2340453831e..0d7c2d20029 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -16,7 +16,7 @@ class AbuseReport < ActiveRecord::Base
def remove_user(deleted_by:)
user.block
- DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
+ DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true)
end
def notify
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 2961e16f5e0..dd1a6922968 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -238,7 +238,8 @@ class ApplicationSetting < ActiveRecord::Base
terminal_max_session_time: 0,
two_factor_grace_period: 48,
user_default_external: false,
- polling_interval_multiplier: 1
+ polling_interval_multiplier: 1,
+ usage_ping_enabled: true
}
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 801d3442803..55872acef51 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -42,12 +42,16 @@ class Blob < SimpleDelegator
size && truncated?
end
+ def extension
+ extname.downcase.delete('.')
+ end
+
def svg?
text? && language && language.name == 'SVG'
end
def pdf?
- name && File.extname(name) == '.pdf'
+ extension == 'pdf'
end
def ipython_notebook?
@@ -55,11 +59,15 @@ class Blob < SimpleDelegator
end
def sketch?
- binary? && extname.downcase.delete('.') == 'sketch'
+ binary? && extension == 'sketch'
end
def stl?
- extname.downcase.delete('.') == 'stl'
+ extension == 'stl'
+ end
+
+ def markup?
+ text? && Gitlab::MarkupHelper.markup?(name)
end
def size_within_svg_limits?
@@ -77,8 +85,10 @@ class Blob < SimpleDelegator
else
'text'
end
- elsif image? || svg?
+ elsif image?
'image'
+ elsif svg?
+ 'svg'
elsif pdf?
'pdf'
elsif ipython_notebook?
@@ -87,8 +97,18 @@ class Blob < SimpleDelegator
'sketch'
elsif stl?
'stl'
+ elsif markup?
+ if only_display_raw?
+ 'too_large'
+ else
+ 'markup'
+ end
elsif text?
- 'text'
+ if only_display_raw?
+ 'too_large'
+ else
+ 'text'
+ end
else
'download'
end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index b59e235c425..2f64f70685a 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -7,7 +7,7 @@ module Ci
belongs_to :project
belongs_to :owner, class_name: "User"
- has_many :trigger_requests, dependent: :destroy
+ has_many :trigger_requests
has_one :trigger_schedule, dependent: :destroy
validates :token, presence: true, uniqueness: true
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 5c452f78546..8b8b3f00202 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -326,14 +326,13 @@ class Commit
end
def raw_diffs(*args)
- use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
- deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
-
- if use_gitaly && !deltas_only
- Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
- else
- raw.diffs(*args)
- end
+ # NOTE: This feature is intentionally disabled until
+ # https://gitlab.com/gitlab-org/gitaly/issues/178 is resolved
+ # if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
+ # Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
+ # else
+ raw.diffs(*args)
+ # end
end
def diffs(diff_options = nil)
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 8ea95beed79..2eedc143968 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -8,6 +8,14 @@
#
# Corresponding foo_html, bar_html and baz_html fields should exist.
module CacheMarkdownField
+ extend ActiveSupport::Concern
+
+ # Increment this number every time the renderer changes its output
+ CACHE_VERSION = 1
+
+ # changes to these attributes cause the cache to be invalidates
+ INVALIDATED_BY = %w[author project].freeze
+
# Knows about the relationship between markdown and html field names, and
# stores the rendering contexts for the latter
class FieldData
@@ -30,60 +38,71 @@ module CacheMarkdownField
end
end
- # Dynamic registries don't really work in Rails as it's not guaranteed that
- # every class will be loaded, so hardcode the list.
- CACHING_CLASSES = %w[
- AbuseReport
- Appearance
- ApplicationSetting
- BroadcastMessage
- Issue
- Label
- MergeRequest
- Milestone
- Namespace
- Note
- Project
- Release
- Snippet
- ].freeze
-
- def self.caching_classes
- CACHING_CLASSES.map(&:constantize)
- end
-
def skip_project_check?
false
end
- extend ActiveSupport::Concern
+ # Returns the default Banzai render context for the cached markdown field.
+ def banzai_render_context(field)
+ raise ArgumentError.new("Unknown field: #{field.inspect}") unless
+ cached_markdown_fields.markdown_fields.include?(field)
- included do
- cattr_reader :cached_markdown_fields do
- FieldData.new
- end
+ # Always include a project key, or Banzai complains
+ project = self.project if self.respond_to?(:project)
+ context = cached_markdown_fields[field].merge(project: project)
- # Returns the default Banzai render context for the cached markdown field.
- def banzai_render_context(field)
- raise ArgumentError.new("Unknown field: #{field.inspect}") unless
- cached_markdown_fields.markdown_fields.include?(field)
+ # Banzai is less strict about authors, so don't always have an author key
+ context[:author] = self.author if self.respond_to?(:author)
- # Always include a project key, or Banzai complains
- project = self.project if self.respond_to?(:project)
- context = cached_markdown_fields[field].merge(project: project)
+ context
+ end
- # Banzai is less strict about authors, so don't always have an author key
- context[:author] = self.author if self.respond_to?(:author)
+ # Update every column in a row if any one is invalidated, as we only store
+ # one version per row
+ def refresh_markdown_cache!(do_update: false)
+ options = { skip_project_check: skip_project_check? }
- context
- end
+ updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
+ [
+ cached_markdown_fields.html_field(markdown_field),
+ Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
+ ]
+ end.to_h
+ updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
- # Allow callers to look up the cache field name, rather than hardcoding it
- def markdown_cache_field_for(field)
- raise ArgumentError.new("Unknown field: #{field}") unless
- cached_markdown_fields.markdown_fields.include?(field)
+ updates.each {|html_field, data| write_attribute(html_field, data) }
- cached_markdown_fields.html_field(field)
+ update_columns(updates) if persisted? && do_update
+ end
+
+ def cached_html_up_to_date?(markdown_field)
+ html_field = cached_markdown_fields.html_field(markdown_field)
+
+ markdown_changed = attribute_changed?(markdown_field) || false
+ html_changed = attribute_changed?(html_field) || false
+
+ CacheMarkdownField::CACHE_VERSION == cached_markdown_version &&
+ (html_changed || markdown_changed == html_changed)
+ end
+
+ def invalidated_markdown_cache?
+ cached_markdown_fields.html_fields.any? {|html_field| attribute_invalidated?(html_field) }
+ end
+
+ def attribute_invalidated?(attr)
+ __send__("#{attr}_invalidated?")
+ end
+
+ def cached_html_for(markdown_field)
+ raise ArgumentError.new("Unknown field: #{field}") unless
+ cached_markdown_fields.markdown_fields.include?(markdown_field)
+
+ __send__(cached_markdown_fields.html_field(markdown_field))
+ end
+
+ included do
+ cattr_reader :cached_markdown_fields do
+ FieldData.new
end
# Always exclude _html fields from attributes (including serialization).
@@ -92,12 +111,16 @@ module CacheMarkdownField
def attributes
attrs = attributes_before_markdown_cache
+ attrs.delete('cached_markdown_version')
+
cached_markdown_fields.html_fields.each do |field|
attrs.delete(field)
end
attrs
end
+
+ before_save :refresh_markdown_cache!, if: :invalidated_markdown_cache?
end
class_methods do
@@ -107,31 +130,18 @@ module CacheMarkdownField
# a corresponding _html field. Any custom rendering options may be provided
# as a context.
def cache_markdown_field(markdown_field, context = {})
- raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
- CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
-
cached_markdown_fields[markdown_field] = context
html_field = cached_markdown_fields.html_field(markdown_field)
- cache_method = "#{markdown_field}_cache_refresh".to_sym
invalidation_method = "#{html_field}_invalidated?".to_sym
- define_method(cache_method) do
- options = { skip_project_check: skip_project_check? }
- html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
- __send__("#{html_field}=", html)
- true
- end
-
# The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
changed_fields = changed_attributes.keys
- invalidations = changed_fields & [markdown_field.to_s, "author", "project"]
- !invalidations.empty?
+ invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
+ !invalidations.empty? || !cached_html_up_to_date?(markdown_field)
end
-
- before_save cache_method, if: invalidation_method
end
end
end
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index 87db0c810c3..8ee42875670 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -2,11 +2,9 @@
module DiscussionOnDiff
extend ActiveSupport::Concern
- included do
- NUMBER_OF_TRUNCATED_DIFF_LINES = 16
-
- memoized_values << :active
+ NUMBER_OF_TRUNCATED_DIFF_LINES = 16
+ included do
delegate :line_code,
:original_line_code,
:diff_file,
@@ -29,12 +27,6 @@ module DiscussionOnDiff
true
end
- def active?
- return @active if @active.present?
-
- @active = first_note.active?
- end
-
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 3d2258d5e3e..26dbf4d9570 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -23,7 +23,7 @@ module Issuable
included do
cache_markdown_field :title, pipeline: :single_line
- cache_markdown_field :description
+ cache_markdown_field :description, issuable_state_filter_enabled: true
belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User"
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index 1a5a7007a2b..6c27dd5aa5c 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -25,4 +25,18 @@ module NoteOnDiff
def diff_attributes
raise NotImplementedError
end
+
+ def active?(diff_refs = nil)
+ raise NotImplementedError
+ end
+
+ private
+
+ def noteable_diff_refs
+ if noteable.respond_to?(:diff_sha_refs)
+ noteable.diff_sha_refs
+ else
+ noteable.diff_refs
+ end
+ end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 772ff6a6d2f..dd1e6630642 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -36,10 +36,10 @@ module Noteable
.discussions(self)
end
- def grouped_diff_discussions
+ def grouped_diff_discussions(*args)
# Doesn't use `discussion_notes`, because this may include commit diff notes
# besides MR diff notes, that we do no want to display on the MR Changes tab.
- notes.inc_relations_for_view.grouped_diff_discussions
+ notes.inc_relations_for_view.grouped_diff_discussions(*args)
end
def resolvable_discussions
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 9682df3a586..d0c94d3b694 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -20,7 +20,12 @@ class ContainerRepository < ActiveRecord::Base
end
def path
- @path ||= [project.full_path, name].select(&:present?).join('/')
+ @path ||= [project.full_path, name]
+ .select(&:present?).join('/').downcase
+ end
+
+ def location
+ File.join(registry.path, path)
end
def tag(tag)
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index d9b7e484e0f..6a6466b493b 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -10,6 +10,7 @@ class DiffDiscussion < Discussion
delegate :position,
:original_position,
+ :latest_merge_request_diff,
to: :first_note
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 1523244f8a8..abe4518d62a 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -65,20 +65,18 @@ class DiffNote < Note
self.position.diff_refs == diff_refs
end
+ def latest_merge_request_diff
+ return unless for_merge_request?
+
+ self.noteable.merge_request_diff_for(self.position.diff_refs)
+ end
+
private
def supported?
for_commit? || self.noteable.has_complete_diff_refs?
end
- def noteable_diff_refs
- if noteable.respond_to?(:diff_sha_refs)
- noteable.diff_sha_refs
- else
- noteable.diff_refs
- end
- end
-
def set_original_position
self.original_position = self.position.dup unless self.original_position&.complete?
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 106084175ff..cbc10b00cf5 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -125,7 +125,7 @@ class Group < Namespace
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- GroupMember.add_users_to_group(
+ GroupMember.add_users(
self,
users,
access_level,
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 3bacc450e6e..920a25932b4 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -7,6 +7,8 @@ class Identity < ActiveRecord::Base
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
validates :user_id, uniqueness: { scope: :provider }
+ scope :with_extern_uid, ->(provider, extern_uid) { where(extern_uid: extern_uid, provider: provider) }
+
def ldap?
provider.starts_with?('ldap')
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d8d9db477d2..305fc01f041 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -26,8 +26,6 @@ class Issue < ActiveRecord::Base
validates :project, presence: true
- scope :cared, ->(user) { where(assignee_id: user) }
- scope :open_for, ->(user) { opened.assigned_to(user) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
scope :without_due_date, -> { where(due_date: nil) }
@@ -201,7 +199,7 @@ class Issue < ActiveRecord::Base
# Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user.
def visible_to_user?(user = nil)
- return false unless project.feature_available?(:issues, user)
+ return false unless project && project.feature_available?(:issues, user)
user ? readable_by?(user) : publicly_visible?
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 568fa6d44f5..d8b0e250732 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -21,6 +21,8 @@ class Label < ActiveRecord::Base
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
+ before_validation :strip_whitespace_from_title_and_color
+
validates :color, color: true, allow_blank: false
# Don't allow ',' for label titles
@@ -193,4 +195,8 @@ class Label < ActiveRecord::Base
def sanitize_title(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s))
end
+
+ def strip_whitespace_from_title_and_color
+ %w(color title).each { |attr| self[attr] = self[attr]&.strip }
+ end
end
diff --git a/app/models/legacy_diff_discussion.rb b/app/models/legacy_diff_discussion.rb
index cb2651a03f8..e617ce36f56 100644
--- a/app/models/legacy_diff_discussion.rb
+++ b/app/models/legacy_diff_discussion.rb
@@ -7,6 +7,8 @@
class LegacyDiffDiscussion < Discussion
include DiscussionOnDiff
+ memoized_values << :active
+
def legacy_diff_discussion?
true
end
@@ -15,6 +17,12 @@ class LegacyDiffDiscussion < Discussion
LegacyDiffNote
end
+ def active?(*args)
+ return @active if @active.present?
+
+ @active = first_note.active?(*args)
+ end
+
def collapsed?
!active?
end
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 9a77557ebcd..d7c627432d2 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -56,11 +56,12 @@ class LegacyDiffNote < Note
#
# If the note's current diff cannot be matched in the MergeRequest's current
# diff, it's considered inactive.
- def active?
+ def active?(diff_refs = nil)
return @active if defined?(@active)
return true if for_commit?
return true unless diff_line
return false unless noteable
+ return false if diff_refs && diff_refs != noteable_diff_refs
noteable_diff = find_noteable_diff
diff --git a/app/models/member.rb b/app/models/member.rb
index 0545bd4eedf..97fba501759 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -151,6 +151,22 @@ class Member < ActiveRecord::Base
member
end
+ def add_users(source, users, access_level, current_user: nil, expires_at: nil)
+ return [] unless users.present?
+
+ self.transaction do
+ users.map do |user|
+ add_user(
+ source,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
+ end
+ end
+ end
+
def access_levels
Gitlab::Access.sym_options
end
@@ -173,18 +189,6 @@ class Member < ActiveRecord::Base
# There is no current user for bulk actions, in which case anything is allowed
!current_user || current_user.can?(:"update_#{member.type.underscore}", member)
end
-
- def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
- users.each do |user|
- add_user(
- source,
- user,
- access_level,
- current_user: current_user,
- expires_at: expires_at
- )
- end
- end
end
def real_source_type
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 483425cd30f..28e10bc6172 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -21,18 +21,6 @@ class GroupMember < Member
Gitlab::Access.sym_options_with_owner
end
- def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil)
- self.transaction do
- add_users_to_source(
- group,
- users,
- access_level,
- current_user: current_user,
- expires_at: expires_at
- )
- end
- end
-
def group
source
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 912820b51ac..b3a91feb091 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -16,7 +16,7 @@ class ProjectMember < Member
before_destroy :delete_member_todos
class << self
- # Add users to project teams with passed access option
+ # Add users to projects with passed access option
#
# access can be an integer representing a access code
# or symbol like :master representing role
@@ -39,7 +39,7 @@ class ProjectMember < Member
project_ids.each do |project_id|
project = Project.find(project_id)
- add_users_to_source(
+ add_users(
project,
users,
access_level,
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index b71a9e17a93..1d4827375d7 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -104,7 +104,6 @@ class MergeRequest < ActiveRecord::Base
scope :by_source_or_target_branch, ->(branch_name) do
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
end
- scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :from_project, ->(project) { where(source_project_id: project.id) }
@@ -367,6 +366,14 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff(true)
end
+ def merge_request_diff_for(diff_refs)
+ @merge_request_diffs_by_diff_refs ||= Hash.new do |h, diff_refs|
+ h[diff_refs] = merge_request_diffs.viewable.select_without_diff.find_by_diff_refs(diff_refs)
+ end
+
+ @merge_request_diffs_by_diff_refs[diff_refs]
+ end
+
def reload_diff_if_branch_changed
if source_branch_changed? || target_branch_changed?
reload_diff
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 6ad56b842b2..6604af2b47e 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -31,6 +31,10 @@ class MergeRequestDiff < ActiveRecord::Base
# It allows you to override variables like head_commit_sha before getting diff.
after_create :save_git_content, unless: :importing?
+ def self.find_by_diff_refs(diff_refs)
+ 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 self.select_without_diff
select(column_names - ['st_diffs'])
end
@@ -130,6 +134,12 @@ class MergeRequestDiff < ActiveRecord::Base
st_commits.map { |commit| commit[:id] }
end
+ def diff_refs=(new_diff_refs)
+ self.base_commit_sha = new_diff_refs&.base_sha
+ self.start_commit_sha = new_diff_refs&.start_sha
+ self.head_commit_sha = new_diff_refs&.head_sha
+ end
+
def diff_refs
return unless start_commit_sha || base_commit_sha
diff --git a/app/models/note.rb b/app/models/note.rb
index 1ea7b946061..e720bfba030 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -16,7 +16,7 @@ class Note < ActiveRecord::Base
ignore_column :original_discussion_id
- cache_markdown_field :note, pipeline: :note
+ cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
# Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer.
@@ -96,6 +96,7 @@ class Note < ActiveRecord::Base
before_validation :set_discussion_id, on: :create
after_save :keep_around_commit, unless: :for_personal_snippet?
after_save :expire_etag_cache
+ after_destroy :expire_etag_cache
class << self
def model_name
@@ -113,11 +114,11 @@ class Note < ActiveRecord::Base
Discussion.build(notes)
end
- def grouped_diff_discussions
+ def grouped_diff_discussions(diff_refs = nil)
diff_notes.
fresh.
discussions.
- select(&:active?).
+ select { |n| n.active?(diff_refs) }.
group_by(&:line_code)
end
@@ -140,6 +141,10 @@ class Note < ActiveRecord::Base
true
end
+ def latest_merge_request_diff
+ nil
+ end
+
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
diff --git a/app/models/project.rb b/app/models/project.rb
index a160efba912..73593f04283 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -181,7 +181,7 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true
- delegate :add_user, to: :team
+ delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
delegate :empty_repo?, to: :repository
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index fa782c6fbb7..f2dfb87dbda 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -22,7 +22,7 @@ class ChatNotificationService < Service
end
def can_test?
- valid?
+ super && valid?
end
def self.supported_events
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 6d6644053f8..543b9b293e0 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -50,8 +50,8 @@ class ProjectTeam
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- ProjectMember.add_users_to_projects(
- [project.id],
+ ProjectMember.add_users(
+ project,
users,
access_level,
current_user: current_user,
diff --git a/app/models/repository.rb b/app/models/repository.rb
index f4c51cdfdf4..7bb874d7744 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -19,7 +19,7 @@ class Repository
#
# For example, for entry `:readme` there's a method called `readme` which
# stores its data in the `readme` cache key.
- CACHED_METHODS = %i(size commit_count readme version contribution_guide
+ CACHED_METHODS = %i(size commit_count readme contribution_guide
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref).freeze
@@ -32,7 +32,6 @@ class Repository
changelog: :changelog,
license: %i(license_blob license_key),
contributing: :contribution_guide,
- version: :version,
gitignore: :gitignore,
koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml,
@@ -109,7 +108,7 @@ class Repository
offset: offset,
after: after,
before: before,
- follow: path.present?,
+ follow: Array(path).length == 1,
skip_merges: skip_merges
}
@@ -407,8 +406,6 @@ class Repository
# Runs code after a repository has been forked/imported.
def after_import
expire_content_cache
- expire_tags_cache
- expire_branches_cache
end
# Runs code after a new commit has been pushed.
@@ -532,11 +529,6 @@ class Repository
end
cache_method :readme
- def version
- file_on_head(:version)
- end
- cache_method :version
-
def contribution_guide
file_on_head(:contributing)
end
@@ -965,13 +957,15 @@ class Repository
end
def is_ancestor?(ancestor_id, descendant_id)
- Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
- if is_enabled
- raw_repository.is_ancestor?(ancestor_id, descendant_id)
- else
- merge_base_commit(ancestor_id, descendant_id) == ancestor_id
- end
- end
+ # NOTE: This feature is intentionally disabled until
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/30586 is resolved
+ # Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
+ # if is_enabled
+ # raw_repository.is_ancestor?(ancestor_id, descendant_id)
+ # else
+ merge_base_commit(ancestor_id, descendant_id) == ancestor_id
+ # end
+ # end
end
def empty_repo?
diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb
index 3b8b9833565..dd21ee15c6c 100644
--- a/app/models/spam_log.rb
+++ b/app/models/spam_log.rb
@@ -3,9 +3,9 @@ class SpamLog < ActiveRecord::Base
validates :user, presence: true
- def remove_user
+ def remove_user(deleted_by:)
user.block
- user.destroy
+ DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true)
end
def text
diff --git a/app/models/user.rb b/app/models/user.rb
index 31e975b8e53..774d4caa806 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -99,9 +99,6 @@ class User < ActiveRecord::Base
has_many :award_emoji, dependent: :destroy
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
- has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
- has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
-
# Issues that a user owns are expected to be moved to the "ghost" user before
# the user is destroyed. If the user owns any issues during deletion, this
# should be treated as an exceptional condition.
@@ -197,7 +194,7 @@ class User < ActiveRecord::Base
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
- scope :active, -> { with_state(:active) }
+ scope :active, -> { with_state(:active).non_internal }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
@@ -587,10 +584,6 @@ class User < ActiveRecord::Base
name.split.first unless name.blank?
end
- def cared_merge_requests
- MergeRequest.cared(self)
- end
-
def projects_limit_left
projects_limit - personal_projects.count
end
@@ -895,20 +888,20 @@ class User < ActiveRecord::Base
@global_notification_setting
end
- def assigned_open_merge_request_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do
- assigned_merge_requests.opened.count
+ def assigned_open_merge_requests_count(force: false)
+ Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do
+ MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
def assigned_open_issues_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
- assigned_issues.opened.count
+ IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
def update_cache_counts
- assigned_open_merge_request_count(force: true)
+ assigned_open_merge_requests_count(force: true)
assigned_open_issues_count(force: true)
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index cb58c115d54..87398303c68 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -28,6 +28,7 @@ class GroupPolicy < BasePolicy
can! :admin_namespace
can! :admin_group_member
can! :change_visibility_level
+ can! :create_subgroup if @user.can_create_group
end
if globally_viewable && @subject.request_access_enabled && !member
diff --git a/app/serializers/cohort_activity_month_entity.rb b/app/serializers/cohort_activity_month_entity.rb
new file mode 100644
index 00000000000..e6788a8b596
--- /dev/null
+++ b/app/serializers/cohort_activity_month_entity.rb
@@ -0,0 +1,11 @@
+class CohortActivityMonthEntity < Grape::Entity
+ include ActionView::Helpers::NumberHelper
+
+ expose :total do |cohort_activity_month|
+ number_with_delimiter(cohort_activity_month[:total])
+ end
+
+ expose :percentage do |cohort_activity_month|
+ number_to_percentage(cohort_activity_month[:percentage], precision: 0)
+ end
+end
diff --git a/app/serializers/cohort_entity.rb b/app/serializers/cohort_entity.rb
new file mode 100644
index 00000000000..7cdba5b0484
--- /dev/null
+++ b/app/serializers/cohort_entity.rb
@@ -0,0 +1,17 @@
+class CohortEntity < Grape::Entity
+ include ActionView::Helpers::NumberHelper
+
+ expose :registration_month do |cohort|
+ cohort[:registration_month].strftime('%b %Y')
+ end
+
+ expose :total do |cohort|
+ number_with_delimiter(cohort[:total])
+ end
+
+ expose :inactive do |cohort|
+ number_with_delimiter(cohort[:inactive])
+ end
+
+ expose :activity_months, using: CohortActivityMonthEntity
+end
diff --git a/app/serializers/cohorts_entity.rb b/app/serializers/cohorts_entity.rb
new file mode 100644
index 00000000000..98f5995ba6f
--- /dev/null
+++ b/app/serializers/cohorts_entity.rb
@@ -0,0 +1,4 @@
+class CohortsEntity < Grape::Entity
+ expose :months_included
+ expose :cohorts, using: CohortEntity
+end
diff --git a/app/serializers/cohorts_serializer.rb b/app/serializers/cohorts_serializer.rb
new file mode 100644
index 00000000000..fe9367b13d8
--- /dev/null
+++ b/app/serializers/cohorts_serializer.rb
@@ -0,0 +1,3 @@
+class CohortsSerializer < AnalyticsGenericSerializer
+ entity CohortsEntity
+end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index dfd9d1584a1..944472f3e51 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -1,8 +1,12 @@
class StatusEntity < Grape::Entity
include RequestAwareEntity
- expose :icon, :favicon, :text, :label, :group
+ expose :icon, :text, :label, :group
expose :has_details?, as: :has_details
expose :details_path
+
+ expose :favicon do |status|
+ ActionController::Base.helpers.image_path(File.join('ci_favicons', "#{status.favicon}.ico"))
+ end
end
diff --git a/app/services/cohorts_service.rb b/app/services/cohorts_service.rb
new file mode 100644
index 00000000000..6781533af28
--- /dev/null
+++ b/app/services/cohorts_service.rb
@@ -0,0 +1,100 @@
+class CohortsService
+ MONTHS_INCLUDED = 12
+
+ def execute
+ {
+ months_included: MONTHS_INCLUDED,
+ cohorts: cohorts
+ }
+ end
+
+ # Get an array of hashes that looks like:
+ #
+ # [
+ # {
+ # registration_month: Date.new(2017, 3),
+ # activity_months: [3, 2, 1],
+ # total: 3
+ # inactive: 0
+ # },
+ # etc.
+ #
+ # The `months` array is always from oldest to newest, so it's always
+ # non-strictly decreasing from left to right.
+ def cohorts
+ months = Array.new(MONTHS_INCLUDED) { |i| i.months.ago.beginning_of_month.to_date }
+
+ Array.new(MONTHS_INCLUDED) do
+ registration_month = months.last
+ activity_months = running_totals(months, registration_month)
+
+ # Even if no users registered in this month, we always want to have a
+ # value to fill in the table.
+ inactive = counts_by_month[[registration_month, nil]].to_i
+
+ months.pop
+
+ {
+ registration_month: registration_month,
+ activity_months: activity_months,
+ total: activity_months.first[:total],
+ inactive: inactive
+ }
+ end
+ end
+
+ private
+
+ # Calculate a running sum of active users, so users active in later months
+ # count as active in this month, too. Start with the most recent month first,
+ # for calculating the running totals, and then reverse for displaying in the
+ # table.
+ #
+ # Each month has a total, and a percentage of the overall total, as keys.
+ def running_totals(all_months, registration_month)
+ month_totals =
+ all_months
+ .map { |activity_month| counts_by_month[[registration_month, activity_month]] }
+ .reduce([]) { |result, total| result << result.last.to_i + total.to_i }
+ .reverse
+
+ overall_total = month_totals.first
+
+ month_totals.map do |total|
+ { total: total, percentage: total.zero? ? 0 : 100 * total / overall_total }
+ end
+ end
+
+ # Get a hash that looks like:
+ #
+ # {
+ # [created_at_month, last_activity_on_month] => count,
+ # [created_at_month, last_activity_on_month_2] => count_2,
+ # # etc.
+ # }
+ #
+ # created_at_month can never be nil, but last_activity_on_month can (when a
+ # user has never logged in, just been created). This covers the last
+ # MONTHS_INCLUDED months.
+ def counts_by_month
+ @counts_by_month ||=
+ begin
+ created_at_month = column_to_date('created_at')
+ last_activity_on_month = column_to_date('last_activity_on')
+
+ User
+ .where('created_at > ?', MONTHS_INCLUDED.months.ago.end_of_month)
+ .group(created_at_month, last_activity_on_month)
+ .reorder("#{created_at_month} ASC", "#{last_activity_on_month} ASC")
+ .count
+ end
+ end
+
+ def column_to_date(column)
+ if Gitlab::Database.postgresql?
+ "CAST(DATE_TRUNC('month', #{column}) AS date)"
+ else
+ "STR_TO_DATE(DATE_FORMAT(#{column}, '%Y-%m-01'), '%Y-%m-%d')"
+ end
+ end
+end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 1297a792259..a48d6a976f0 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -1,69 +1,27 @@
module Commits
- class ChangeService < ::BaseService
- ValidationError = Class.new(StandardError)
- ChangeError = Class.new(StandardError)
+ class ChangeService < Commits::CreateService
+ def initialize(*args)
+ super
- def execute
- @start_project = params[:start_project] || @project
- @start_branch = params[:start_branch]
- @target_branch = params[:target_branch]
@commit = params[:commit]
-
- check_push_permissions
-
- commit
- rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
- ValidationError, ChangeError => ex
- error(ex.message)
end
private
- def commit
- raise NotImplementedError
- end
-
def commit_change(action)
raise NotImplementedError unless repository.respond_to?(action)
- validate_target_branch if different_branch?
-
repository.public_send(
action,
current_user,
@commit,
- @target_branch,
+ @branch_name,
start_project: @start_project,
start_branch_name: @start_branch)
-
- success
rescue Repository::CreateTreeError
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
- A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content."
+ This #{@commit.change_type_title(current_user)} may already have been #{action.to_s.dasherize}ed, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg
end
-
- def check_push_permissions
- allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
-
- unless allowed
- raise ValidationError.new('You are not allowed to push into this branch')
- end
-
- true
- end
-
- def validate_target_branch
- result = ValidateNewBranchService.new(@project, current_user)
- .execute(@target_branch)
-
- if result[:status] == :error
- raise ChangeError, "There was an error creating the source branch: #{result[:message]}"
- end
- end
-
- def different_branch?
- @start_branch != @target_branch || @start_project != @project
- end
end
end
diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb
index 605cca36f9c..320e229560d 100644
--- a/app/services/commits/cherry_pick_service.rb
+++ b/app/services/commits/cherry_pick_service.rb
@@ -1,6 +1,6 @@
module Commits
class CherryPickService < ChangeService
- def commit
+ def create_commit!
commit_change(:cherry_pick)
end
end
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
new file mode 100644
index 00000000000..c58f04a252b
--- /dev/null
+++ b/app/services/commits/create_service.rb
@@ -0,0 +1,74 @@
+module Commits
+ class CreateService < ::BaseService
+ ValidationError = Class.new(StandardError)
+ ChangeError = Class.new(StandardError)
+
+ def initialize(*args)
+ super
+
+ @start_project = params[:start_project] || @project
+ @start_branch = params[:start_branch]
+ @branch_name = params[:branch_name]
+ end
+
+ def execute
+ validate!
+
+ new_commit = create_commit!
+
+ success(result: new_commit)
+ rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Repository::CommitError, GitHooksService::PreReceiveError => ex
+ error(ex.message)
+ end
+
+ private
+
+ def create_commit!
+ raise NotImplementedError
+ end
+
+ def raise_error(message)
+ raise ValidationError, message
+ end
+
+ def different_branch?
+ @start_branch != @branch_name || @start_project != @project
+ end
+
+ def validate!
+ validate_permissions!
+ validate_on_branch!
+ validate_branch_existance!
+
+ validate_new_branch_name! if different_branch?
+ end
+
+ def validate_permissions!
+ allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@branch_name)
+
+ unless allowed
+ raise_error("You are not allowed to push into this branch")
+ end
+ end
+
+ def validate_on_branch!
+ if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch)
+ raise_error('You can only create or edit files when you are on a branch')
+ end
+ end
+
+ def validate_branch_existance!
+ if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name)
+ raise_error("A branch called '#{@branch_name}' already exists. Switch to that branch in order to make changes")
+ end
+ end
+
+ def validate_new_branch_name!
+ result = ValidateNewBranchService.new(project, current_user).execute(@branch_name)
+
+ if result[:status] == :error
+ raise_error("Something went wrong when we tried to create '#{@branch_name}' for you: #{result[:message]}")
+ end
+ end
+ end
+end
diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb
index addd55cb32f..dc27399e047 100644
--- a/app/services/commits/revert_service.rb
+++ b/app/services/commits/revert_service.rb
@@ -1,6 +1,6 @@
module Commits
class RevertService < ChangeService
- def commit
+ def create_commit!
commit_change(:revert)
end
end
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
index 1b5623baebe..3b611588466 100644
--- a/app/services/delete_merged_branches_service.rb
+++ b/app/services/delete_merged_branches_service.rb
@@ -8,9 +8,20 @@ class DeleteMergedBranchesService < BaseService
branches = project.repository.branch_names
branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
+ # Prevent deletion of branches relevant to open merge requests
+ branches -= merge_request_branch_names
branches.each do |branch|
DeleteBranchService.new(project, current_user).execute(branch)
end
end
+
+ private
+
+ def merge_request_branch_names
+ # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY
+ source_names = project.origin_merge_requests.opened.reorder(nil).uniq.pluck(:source_branch)
+ target_names = project.merge_requests.opened.reorder(nil).uniq.pluck(:target_branch)
+ (source_names + target_names).uniq
+ end
end
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index e24cc66e0fe..0f3a485a3fd 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -72,6 +72,8 @@ class EventCreateService
def push(project, current_user, push_data)
create_event(project, current_user, Event::PUSHED, data: push_data)
+
+ Users::ActivityService.new(current_user, 'push').execute
end
private
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index c8a60422bf4..38231f66009 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -1,79 +1,17 @@
module Files
- class BaseService < ::BaseService
- ValidationError = Class.new(StandardError)
-
- def execute
- @start_project = params[:start_project] || @project
- @start_branch = params[:start_branch]
- @target_branch = params[:target_branch]
+ class BaseService < Commits::CreateService
+ def initialize(*args)
+ super
+ @author_email = params[:author_email]
+ @author_name = params[:author_name]
@commit_message = params[:commit_message]
- @file_path = params[:file_path]
- @previous_path = params[:previous_path]
- @file_content = if params[:file_content_encoding] == 'base64'
- Base64.decode64(params[:file_content])
- else
- params[:file_content]
- end
- @last_commit_sha = params[:last_commit_sha]
- @author_email = params[:author_email]
- @author_name = params[:author_name]
-
- # Validate parameters
- validate
-
- # Create new branch if it different from start_branch
- validate_target_branch if different_branch?
-
- result = commit
- if result
- success(result: result)
- else
- error('Something went wrong. Your changes were not committed')
- end
- rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError => ex
- error(ex.message)
- end
-
- private
-
- def different_branch?
- @start_branch != @target_branch || @start_project != @project
- end
-
- def file_has_changed?
- return false unless @last_commit_sha && last_commit
-
- @last_commit_sha != last_commit.sha
- end
-
- def raise_error(message)
- raise ValidationError.new(message)
- end
-
- def validate
- allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
-
- unless allowed
- raise_error("You are not allowed to push into this branch")
- end
-
- if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch)
- raise ValidationError, 'You can only create or edit files when you are on a branch'
- end
-
- if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name)
- raise ValidationError, "A branch called #{@branch_name} already exists. Switch to that branch in order to make changes"
- end
- end
- def validate_target_branch
- result = ValidateNewBranchService.new(project, current_user).
- execute(@target_branch)
+ @file_path = params[:file_path]
+ @previous_path = params[:previous_path]
- if result[:status] == :error
- raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}")
- end
+ @file_content = params[:file_content]
+ @file_content = Base64.decode64(@file_content) if params[:file_content_encoding] == 'base64'
end
end
end
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index 083ffdc634c..8ecac6115bd 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -1,26 +1,15 @@
module Files
class CreateDirService < Files::BaseService
- def commit
+ def create_commit!
repository.create_dir(
current_user,
@file_path,
message: @commit_message,
- branch_name: @target_branch,
+ branch_name: @branch_name,
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
start_branch_name: @start_branch)
end
-
- def validate
- super
-
- unless @file_path =~ Gitlab::Regex.file_path_regex
- raise_error(
- 'Your changes could not be committed, because the file path ' +
- Gitlab::Regex.file_path_regex_message
- )
- end
- end
end
end
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 65b5537fb68..00a8dcf0934 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,48 +1,16 @@
module Files
class CreateService < Files::BaseService
- def commit
+ def create_commit!
repository.create_file(
current_user,
@file_path,
@file_content,
message: @commit_message,
- branch_name: @target_branch,
+ branch_name: @branch_name,
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
start_branch_name: @start_branch)
end
-
- def validate
- super
-
- if @file_content.nil?
- raise_error("You must provide content.")
- end
-
- if @file_path =~ Gitlab::Regex.directory_traversal_regex
- raise_error(
- 'Your changes could not be committed, because the file name ' +
- Gitlab::Regex.directory_traversal_regex_message
- )
- end
-
- unless @file_path =~ Gitlab::Regex.file_path_regex
- raise_error(
- 'Your changes could not be committed, because the file name ' +
- Gitlab::Regex.file_path_regex_message
- )
- end
-
- unless project.empty_repo?
- @file_path.slice!(0) if @file_path.start_with?('/')
-
- blob = repository.blob_at_branch(@start_branch, @file_path)
-
- if blob
- raise_error('Your changes could not be committed because a file with the same name already exists')
- end
- end
- end
end
end
diff --git a/app/services/files/destroy_service.rb b/app/services/files/delete_service.rb
index e294659bc98..7952e5c95d4 100644
--- a/app/services/files/destroy_service.rb
+++ b/app/services/files/delete_service.rb
@@ -1,11 +1,11 @@
module Files
- class DestroyService < Files::BaseService
- def commit
+ class DeleteService < Files::BaseService
+ def create_commit!
repository.delete_file(
current_user,
@file_path,
message: @commit_message,
- branch_name: @target_branch,
+ branch_name: @branch_name,
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index 700f9f4f6f0..bfacc462847 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -1,14 +1,10 @@
module Files
class MultiService < Files::BaseService
- FileChangedError = Class.new(StandardError)
-
- ACTIONS = %w[create update delete move].freeze
-
- def commit
+ def create_commit!
repository.multi_action(
user: current_user,
message: @commit_message,
- branch_name: @target_branch,
+ branch_name: @branch_name,
actions: params[:actions],
author_email: @author_email,
author_name: @author_name,
@@ -19,122 +15,17 @@ module Files
private
- def validate
+ def validate!
super
- params[:actions].each_with_index do |action, index|
- if ACTIONS.include?(action[:action].to_s)
- action[:action] = action[:action].to_sym
- else
- raise_error("Unknown action type `#{action[:action]}`.")
- end
-
- unless action[:file_path].present?
- raise_error("You must specify a file_path.")
- end
-
- action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
- action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
-
- regex_check(action[:file_path])
- regex_check(action[:previous_path]) if action[:previous_path]
-
- if project.empty_repo? && action[:action] != :create
- raise_error("No files to #{action[:action]}.")
- end
-
- validate_file_exists(action)
-
- case action[:action]
- when :create
- validate_create(action)
- when :update
- validate_update(action)
- when :delete
- validate_delete(action)
- when :move
- validate_move(action, index)
- end
- end
- end
-
- def validate_file_exists(action)
- return if action[:action] == :create
-
- file_path = action[:file_path]
- file_path = action[:previous_path] if action[:action] == :move
-
- blob = repository.blob_at_branch(params[:branch], file_path)
-
- unless blob
- raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
+ params[:actions].each do |action|
+ validate_action!(action)
end
end
- def last_commit
- Gitlab::Git::Commit.last_for_path(repository, @start_branch, @file_path)
- end
-
- def regex_check(file)
- if file =~ Gitlab::Regex.directory_traversal_regex
- raise_error(
- 'Your changes could not be committed, because the file name, `' +
- file +
- '` ' +
- Gitlab::Regex.directory_traversal_regex_message
- )
- end
-
- unless file =~ Gitlab::Regex.file_path_regex
- raise_error(
- 'Your changes could not be committed, because the file name, `' +
- file +
- '` ' +
- Gitlab::Regex.file_path_regex_message
- )
- end
- end
-
- def validate_create(action)
- return if project.empty_repo?
-
- if repository.blob_at_branch(params[:branch], action[:file_path])
- raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
- end
-
- if action[:content].nil?
- raise_error("You must provide content.")
- end
- end
-
- def validate_update(action)
- if action[:content].nil?
- raise_error("You must provide content.")
- end
-
- if file_has_changed?
- raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
- end
- end
-
- def validate_delete(action)
- end
-
- def validate_move(action, index)
- if action[:previous_path].nil?
- raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
- end
-
- blob = repository.blob_at_branch(params[:branch], action[:file_path])
-
- if blob
- raise_error("Move destination `#{action[:file_path]}` already exists.")
- end
-
- if action[:content].nil?
- blob = repository.blob_at_branch(params[:branch], action[:previous_path])
- blob.load_all_data!(repository) if blob.truncated?
- params[:actions][index][:content] = blob.data
+ def validate_action!(action)
+ unless Gitlab::Git::Index::ACTIONS.include?(action[:action].to_s)
+ raise_error("Unknown action '#{action[:action]}'")
end
end
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index fbbab97632e..f23a9f6d57c 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -2,10 +2,16 @@ module Files
class UpdateService < Files::BaseService
FileChangedError = Class.new(StandardError)
- def commit
+ def initialize(*args)
+ super
+
+ @last_commit_sha = params[:last_commit_sha]
+ end
+
+ def create_commit!
repository.update_file(current_user, @file_path, @file_content,
message: @commit_message,
- branch_name: @target_branch,
+ branch_name: @branch_name,
previous_path: @previous_path,
author_email: @author_email,
author_name: @author_name,
@@ -15,21 +21,23 @@ module Files
private
- def validate
- super
-
- if @file_content.nil?
- raise_error("You must provide content.")
- end
+ def file_has_changed?
+ return false unless @last_commit_sha && last_commit
- if file_has_changed?
- raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
- end
+ @last_commit_sha != last_commit.sha
end
def last_commit
@last_commit ||= Gitlab::Git::Commit.
last_for_path(@start_project.repository, @start_branch, @file_path)
end
+
+ def validate!
+ super
+
+ if file_has_changed?
+ raise FileChangedError, "You are attempting to update a file that has changed since you started editing it."
+ end
+ end
end
end
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
index b7a244c2029..1711be7211c 100644
--- a/app/services/members/authorized_destroy_service.rb
+++ b/app/services/members/authorized_destroy_service.rb
@@ -9,7 +9,11 @@ module Members
def execute
return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
- member.destroy
+ Member.transaction do
+ unassign_issues_and_merge_requests(member)
+
+ member.destroy
+ end
if member.request? && member.user != user
notification_service.decline_access_request(member)
@@ -17,5 +21,23 @@ module Members
member
end
+
+ private
+
+ def unassign_issues_and_merge_requests(member)
+ if member.is_a?(GroupMember)
+ IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
+ execute.
+ update_all(assignee_id: nil)
+ MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
+ execute.
+ update_all(assignee_id: nil)
+ else
+ project = member.source
+ project.issues.opened.assigned_to(member.user).update_all(assignee_id: nil)
+ project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
+ member.user.update_cache_counts
+ end
+ end
end
end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index e4b24ccef92..3a58f6c065d 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -1,9 +1,15 @@
module Members
class CreateService < BaseService
+ def initialize(source, current_user, params = {})
+ @source = source
+ @current_user = current_user
+ @params = params
+ end
+
def execute
return false if params[:user_ids].blank?
- project.team.add_users(
+ @source.add_users(
params[:user_ids].split(','),
params[:access_level],
expires_at: params[:expires_at],
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index fbdaa455651..7828c5806b0 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -58,6 +58,9 @@ module Projects
fail(error: @project.errors.full_messages.join(', '))
end
@project
+ rescue ActiveRecord::RecordInvalid => e
+ message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} "
+ fail(error: message)
rescue => e
fail(error: e.message)
end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 4c72d5e117d..eea17e24903 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -59,7 +59,6 @@ module Projects
project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true)
- project.repository.remove_remote(project.import_type)
end
def import_data
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index 8409b592b72..ff188102b62 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -7,16 +7,13 @@ module Search
end
def execute
- group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
- projects = ProjectsFinder.new(current_user: current_user).execute
-
- if group
- projects = projects.inside_path(group.full_path)
- end
-
Gitlab::SearchResults.new(current_user, projects, params[:search])
end
+ def projects
+ @projects ||= ProjectsFinder.new(current_user: current_user).execute
+ end
+
def scope
@scope ||= begin
allowed_scopes = %w[issues merge_requests milestones]
diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb
new file mode 100644
index 00000000000..29478e3251f
--- /dev/null
+++ b/app/services/search/group_service.rb
@@ -0,0 +1,18 @@
+module Search
+ class GroupService < Search::GlobalService
+ attr_accessor :group
+
+ def initialize(user, group, params)
+ super(user, params)
+
+ @group = group
+ end
+
+ def projects
+ return Project.none unless group
+ return @projects if defined? @projects
+
+ @projects = super.inside_path(group.full_path)
+ end
+ end
+end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 8d46a8dab3e..22736c71725 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -54,6 +54,8 @@ class SearchService
Search::ProjectService.new(project, current_user, params)
elsif show_snippets?
Search::SnippetService.new(current_user, params)
+ elsif group
+ Search::GroupService.new(current_user, group, params)
else
Search::GlobalService.new(current_user, params)
end
diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb
new file mode 100644
index 00000000000..facf21a7f5c
--- /dev/null
+++ b/app/services/users/activity_service.rb
@@ -0,0 +1,22 @@
+module Users
+ class ActivityService
+ def initialize(author, activity)
+ @author = author.respond_to?(:user) ? author.user : author
+ @activity = activity
+ end
+
+ def execute
+ return unless @author && @author.is_a?(User)
+
+ record_activity
+ end
+
+ private
+
+ def record_activity
+ Gitlab::UserActivities.record(@author.id)
+
+ Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}")
+ end
+ end
+end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
new file mode 100644
index 00000000000..9a0a5a12f91
--- /dev/null
+++ b/app/services/users/build_service.rb
@@ -0,0 +1,100 @@
+module Users
+ # Service for building a new user.
+ class BuildService < BaseService
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params.dup
+ end
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can_create_user?
+
+ user = User.new(build_user_params)
+
+ if current_user&.admin?
+ if params[:reset_password]
+ user.generate_reset_token
+ params[:force_random_password] = true
+ end
+
+ if params[:force_random_password]
+ random_password = Devise.friendly_token.first(Devise.password_length.min)
+ user.password = user.password_confirmation = random_password
+ end
+ end
+
+ identity_attrs = params.slice(:extern_uid, :provider)
+
+ if identity_attrs.any?
+ user.identities.build(identity_attrs)
+ end
+
+ user
+ end
+
+ private
+
+ def can_create_user?
+ (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.admin?
+ end
+
+ # Allowed params for creating a user (admins only)
+ def admin_create_params
+ [
+ :access_level,
+ :admin,
+ :avatar,
+ :bio,
+ :can_create_group,
+ :color_scheme_id,
+ :email,
+ :external,
+ :force_random_password,
+ :hide_no_password,
+ :hide_no_ssh_key,
+ :key_id,
+ :linkedin,
+ :name,
+ :password,
+ :password_automatically_set,
+ :password_expires_at,
+ :projects_limit,
+ :remember_me,
+ :skip_confirmation,
+ :skype,
+ :theme_id,
+ :twitter,
+ :username,
+ :website_url
+ ]
+ end
+
+ # Allowed params for user signup
+ def signup_params
+ [
+ :email,
+ :email_confirmation,
+ :password_automatically_set,
+ :name,
+ :password,
+ :username
+ ]
+ end
+
+ def build_user_params
+ if current_user&.admin?
+ user_params = params.slice(*admin_create_params)
+ user_params[:created_by_id] = current_user&.id
+
+ if params[:reset_password]
+ user_params.merge!(force_random_password: true, password_expires_at: nil)
+ end
+ else
+ user_params = params.slice(*signup_params)
+ user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email
+ end
+
+ user_params
+ end
+ end
+end
diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb
index 93ca7b1141a..a2105d31f71 100644
--- a/app/services/users/create_service.rb
+++ b/app/services/users/create_service.rb
@@ -6,34 +6,10 @@ module Users
@params = params.dup
end
- def build
- raise Gitlab::Access::AccessDeniedError unless can_create_user?
-
- user = User.new(build_user_params)
-
- if current_user&.admin?
- if params[:reset_password]
- @reset_token = user.generate_reset_token
- params[:force_random_password] = true
- end
-
- if params[:force_random_password]
- random_password = Devise.friendly_token.first(Devise.password_length.min)
- user.password = user.password_confirmation = random_password
- end
- end
-
- identity_attrs = params.slice(:extern_uid, :provider)
-
- if identity_attrs.any?
- user.identities.build(identity_attrs)
- end
-
- user
- end
-
def execute
- user = build
+ user = Users::BuildService.new(current_user, params).execute
+
+ @reset_token = user.generate_reset_token if user.recently_sent_password_reset?
if user.save
log_info("User \"#{user.name}\" (#{user.email}) was created")
@@ -43,70 +19,5 @@ module Users
user
end
-
- private
-
- def can_create_user?
- (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.admin?
- end
-
- # Allowed params for creating a user (admins only)
- def admin_create_params
- [
- :access_level,
- :admin,
- :avatar,
- :bio,
- :can_create_group,
- :color_scheme_id,
- :email,
- :external,
- :force_random_password,
- :password_automatically_set,
- :hide_no_password,
- :hide_no_ssh_key,
- :key_id,
- :linkedin,
- :name,
- :password,
- :password_expires_at,
- :projects_limit,
- :remember_me,
- :skip_confirmation,
- :skype,
- :theme_id,
- :twitter,
- :username,
- :website_url
- ]
- end
-
- # Allowed params for user signup
- def signup_params
- [
- :email,
- :email_confirmation,
- :password_automatically_set,
- :name,
- :password,
- :username
- ]
- end
-
- def build_user_params
- if current_user&.admin?
- user_params = params.slice(*admin_create_params)
- user_params[:created_by_id] = current_user&.id
-
- if params[:reset_password]
- user_params.merge!(force_random_password: true, password_expires_at: nil)
- end
- else
- user_params = params.slice(*signup_params)
- user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email
- end
-
- user_params
- end
end
end
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index ba58b174cc0..9eb6a600f6b 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -26,7 +26,7 @@ module Users
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end
- MigrateToGhostUserService.new(user).execute
+ MigrateToGhostUserService.new(user).execute unless options[:hard_delete]
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
namespace = user.namespace
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index 1e1ed1791ec..4628c4c6f6e 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -15,27 +15,39 @@ module Users
end
def execute
- # Block the user before moving records to prevent a data race.
- # For example, if the user creates an issue after `migrate_issues`
- # runs and before the user is destroyed, the destroy will fail with
- # an exception.
- user.block
+ transition = user.block_transition
user.transaction do
+ # Block the user before moving records to prevent a data race.
+ # For example, if the user creates an issue after `migrate_issues`
+ # runs and before the user is destroyed, the destroy will fail with
+ # an exception.
+ user.block
+
+ # Reverse the user block if record migration fails
+ if !migrate_records && transition
+ transition.rollback
+ user.save!
+ end
+ end
+
+ user.reload
+ end
+
+ private
+
+ def migrate_records
+ user.transaction(requires_new: true) do
@ghost_user = User.ghost
migrate_issues
migrate_merge_requests
migrate_notes
migrate_abuse_reports
- migrate_award_emoji
+ migrate_award_emojis
end
-
- user.reload
end
- private
-
def migrate_issues
user.issues.update_all(author_id: ghost_user.id)
end
@@ -52,7 +64,7 @@ module Users
user.reported_abuse_reports.update_all(reporter_id: ghost_user.id)
end
- def migrate_award_emoji
+ def migrate_award_emojis
user.award_emoji.update_all(user_id: ghost_user.id)
end
end
diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb
index 2f61be184ce..d232e85cd33 100644
--- a/app/services/validate_new_branch_service.rb
+++ b/app/services/validate_new_branch_service.rb
@@ -8,10 +8,7 @@ class ValidateNewBranchService < BaseService
return error('Branch name is invalid')
end
- repository = project.repository
- existing_branch = repository.find_branch(branch_name)
-
- if existing_branch
+ if project.repository.branch_exists?(branch_name)
return error('Branch already exists')
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 703f611bb45..0dc1103eece 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -477,7 +477,7 @@
diagrams in Asciidoc documents using an external PlantUML service.
%fieldset
- %legend Usage statistics
+ %legend#usage-statistics Usage statistics
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
@@ -486,6 +486,19 @@
Version check enabled
.help-block
Let GitLab inform you when an update is available.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :usage_ping_enabled do
+ = f.check_box :usage_ping_enabled
+ Usage ping enabled
+ = link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-data")
+ .help-block
+ Every week GitLab will report license usage back to GitLab, Inc.
+ Disable this option if you do not want this to occur. To see the
+ JSON payload that will be sent, visit the
+ = succeed '.' do
+ = link_to "Cohorts page", admin_cohorts_path(anchor: 'usage-ping')
%fieldset
%legend Email
@@ -571,6 +584,7 @@
The multiplier can also have a decimal value.
The default value (1) is a reasonable choice for the majority of GitLab
installations. Set to 0 to completely disable polling.
+ = link_to icon('question-circle'), help_page_path('administration/polling')
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/cohorts/_cohorts_table.html.haml b/app/views/admin/cohorts/_cohorts_table.html.haml
new file mode 100644
index 00000000000..701a4e62b39
--- /dev/null
+++ b/app/views/admin/cohorts/_cohorts_table.html.haml
@@ -0,0 +1,28 @@
+.bs-callout.clearfix
+ %p
+ User cohorts are shown for the last #{@cohorts[:months_included]}
+ months. Only users with activity are counted in the cohort total; inactive
+ users are counted separately.
+ = link_to icon('question-circle'), help_page_path('user/admin_area/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank'
+
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th Registration month
+ %th Inactive users
+ %th Cohort total
+ - @cohorts[:months_included].times do |i|
+ %th Month #{i}
+ %tbody
+ - @cohorts[:cohorts].each do |cohort|
+ %tr
+ %td= cohort[:registration_month]
+ %td= cohort[:inactive]
+ %td= cohort[:total]
+ - cohort[:activity_months].each do |activity_month|
+ %td
+ - next if cohort[:total] == '0'
+ = activity_month[:percentage]
+ %br
+ = activity_month[:total]
diff --git a/app/views/admin/cohorts/_usage_ping.html.haml b/app/views/admin/cohorts/_usage_ping.html.haml
new file mode 100644
index 00000000000..73aa95d84f1
--- /dev/null
+++ b/app/views/admin/cohorts/_usage_ping.html.haml
@@ -0,0 +1,10 @@
+%h2#usage-ping Usage ping
+
+.bs-callout.clearfix
+ %p
+ User cohorts are shown because the usage ping is enabled. The data sent with
+ this is shown below. To disable this, visit
+ = succeed '.' do
+ = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
+
+%pre.usage-data.js-syntax-highlight.code.highlight{ data: { endpoint: usage_data_admin_application_settings_path(format: :html, pretty: true) } }
diff --git a/app/views/admin/cohorts/index.html.haml b/app/views/admin/cohorts/index.html.haml
new file mode 100644
index 00000000000..46fe12a5a99
--- /dev/null
+++ b/app/views/admin/cohorts/index.html.haml
@@ -0,0 +1,16 @@
+- @no_container = true
+= render "admin/dashboard/head"
+
+%div{ class: container_class }
+ - if @cohorts
+ = render 'cohorts_table'
+ = render 'usage_ping'
+ - else
+ .bs-callout.bs-callout-warning.clearfix
+ %p
+ User cohorts are only shown when the
+ = link_to 'usage ping', help_page_path('user/admin_area/usage_statistics'), target: '_blank'
+ is enabled. To enable it and see user cohorts,
+ visit
+ = succeed '.' do
+ = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index 7893c1dee97..163bd5662b0 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -27,3 +27,7 @@
= link_to admin_runners_path, title: 'Runners' do
%span
Runners
+ = nav_link path: 'cohorts#index' do
+ = link_to admin_cohorts_path, title: 'Cohorts' do
+ %span
+ Cohorts
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index e79303240f0..6a208d76a38 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -13,7 +13,7 @@
= button_to reset_health_check_token_admin_application_settings_path,
method: :put, class: 'btn btn-default',
data: { confirm: 'Are you sure you want to reset the health check token?' } do
- = icon('refresh')
+ = icon('spinner')
Reset health check access token
%p.light
Health information can be retrieved as plain text, JSON, or XML using:
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 7d26864d0f3..f118804cace 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -21,7 +21,7 @@
= button_to reset_runners_token_admin_application_settings_path,
method: :put, class: 'btn btn-default',
data: { confirm: 'Are you sure you want to reset registration token?' } do
- = icon('refresh')
+ = icon('spinner')
Reset runners registration token
.bs-callout
diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml
index 6a5986f496a..50132572096 100644
--- a/app/views/admin/services/index.html.haml
+++ b/app/views/admin/services/index.html.haml
@@ -13,7 +13,7 @@
- @services.sort_by(&:title).each do |service|
%tr
%td
- = icon("copy", class: 'clgray')
+ = boolean_to_icon service.activated?
%td
= link_to edit_admin_application_settings_service_path(service.id) do
%strong= service.title
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 3ca45fbf751..9aabfb49a29 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -1,8 +1,9 @@
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
+- user_authored = awardable.user_authored?(current_user)
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
- class: (award_state_class(awards, current_user)),
+ class: [(award_state_class(awards, current_user)), (award_user_authored_class(emoji) if user_authored)],
data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji)
%span.award-control-text.js-counter
@@ -12,6 +13,7 @@
.award-menu-holder.js-award-holder
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': 'Add emoji',
+ class: ("js-user-authored" if user_authored),
data: { title: 'Add emoji', placement: "bottom" } }
%span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley')
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index e04958817e4..8440fb3d785 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -20,21 +20,22 @@
= discussion.author.to_reference
started a discussion
+ - url = discussion_diff_path(discussion)
- if discussion.for_commit? && @noteable != discussion.noteable
on
- commit = discussion.noteable
- if commit
commit
- - anchor = discussion.line_code if discussion.diff_discussion?
- = link_to commit.short_id, namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: anchor), class: 'monospace'
+ = link_to commit.short_id, url, class: 'monospace'
- else
a deleted commit
- elsif discussion.diff_discussion?
on
- - if discussion.active?
- = link_to 'the diff', discussion_diff_path(discussion)
- - else
- an outdated diff
+ = conditional_link_to url.present?, url do
+ - if discussion.active?
+ the diff
+ - else
+ an outdated diff
= time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago")
= render "discussions/headline", discussion: discussion
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index af97e9588a5..01e72862114 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -1,13 +1,4 @@
-- if event.target
- - if event.action_name == "opened"
- .profile-icon.open-icon
- = custom_icon("icon_status_open")
- - elsif event.action_name == "closed"
- .profile-icon.closed-icon
- = custom_icon("icon_status_closed")
- - else
- .profile-icon.fork-icon
- = custom_icon("icon_code_fork")
+= icon_for_profile_event(event)
.event-title
%span.author_name= link_to_author event
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index fee85c94277..d8e59be57bb 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -1,5 +1,4 @@
-.profile-icon.open-icon
- = custom_icon("icon_status_open")
+= icon_for_profile_event(event)
.event-title
%span.author_name= link_to_author event
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index 83709f5e4d0..df4b9562215 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -1,5 +1,4 @@
-.profile-icon
- = custom_icon("icon_comment_o")
+= icon_for_profile_event(event)
.event-title
%span.author_name= link_to_author event
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index efdc8764acf..c0943100ae3 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -1,10 +1,6 @@
- project = event.project
-.profile-icon
- - if event.action_name == "deleted"
- = custom_icon("trash_o")
- - else
- = custom_icon("icon_commit")
+= icon_for_profile_event(event)
.event-title
%span.author_name= link_to_author event
diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml
index be809083139..8f0724c0677 100644
--- a/app/views/groups/subgroups.html.haml
+++ b/app/views/groups/subgroups.html.haml
@@ -9,7 +9,7 @@
.nav-controls
= form_tag request.path, method: :get do |f|
= search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name', class: 'form-control', spellcheck: false
- - if can? current_user, :admin_group, @group
+ - if can?(current_user, :create_subgroup, @group)
= link_to new_group_path(parent_id: @group.id), class: 'btn btn-new pull-right' do
New Subgroup
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 700c5e61a14..ea8bbe92d86 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -318,3 +318,11 @@
%td.shortcut
.key l
%td Change Label
+ %tbody.hidden-shortcut.wiki{ style: 'display:none' }
+ %tr
+ %th
+ %th Wiki pages
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit wiki page
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 207f80bedfe..615dd56afbd 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -252,7 +252,7 @@
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
- %span Dropdown Title
+ %span Dropdown title
%button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
@@ -291,7 +291,7 @@
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading
.dropdown-title
- %span Dropdown Title
+ %span Dropdown title
%button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
@@ -335,7 +335,7 @@
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user
.dropdown-title
- %span Dropdown Title
+ %span Dropdown title
%button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
@@ -362,7 +362,7 @@
.dropdown-title
%button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } }
= icon('arrow-left')
- %span Dropdown Title
+ %span Dropdown title
%button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index a611481a0a4..19473b6ab27 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -28,9 +28,9 @@
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
- = javascript_include_tag(*webpack_asset_paths("runtime"))
- = javascript_include_tag(*webpack_asset_paths("common"))
- = javascript_include_tag(*webpack_asset_paths("main"))
+ = webpack_bundle_tag "runtime"
+ = webpack_bundle_tag "common"
+ = webpack_bundle_tag "main"
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index a9893dea68f..659d548df18 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -47,13 +47,13 @@
%li
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('hashtag fw')
- - issues_count = cached_assigned_issuables_count(current_user, :issues, :opened)
+ - issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
%li
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold')
- - merge_requests_count = cached_assigned_issuables_count(current_user, :merge_requests, :opened)
+ - merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
%li
@@ -67,6 +67,11 @@
= icon('caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
+ %li.current-user
+ .user-name.bold
+ = current_user.name
+ @#{current_user.username}
+ %li.divider
%li
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 444ecc414c0..ac222ad8c82 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -44,7 +44,7 @@
I
%span
Issues
- .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
+ .badge= number_with_delimiter(assigned_issuables_count(:issues))
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
.shortcut-mappings
@@ -53,7 +53,7 @@
M
%span
Merge Requests
- .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
+ .badge= number_with_delimiter(assigned_issuables_count(:merge_requests))
= nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
.shortcut-mappings
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index 3a1fcd00e9c..0cb367452f7 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -1,16 +1,29 @@
%ul
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
- = link_to explore_root_path, title: 'Projects' do
+ = link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
+ .shortcut-mappings
+ .key
+ = icon('arrow-up', 'aria-label' => 'hidden')
+ P
%span
Projects
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
- = link_to explore_groups_path, title: 'Groups' do
+ = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
+ .shortcut-mappings
+ .key
+ = icon('arrow-up', 'aria-label' => 'hidden')
+ G
%span
Groups
= nav_link(controller: :snippets) do
- = link_to explore_snippets_path, title: 'Snippets' do
+ = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
+ .shortcut-mappings
+ .key
+ = icon('arrow-up', 'aria-label' => 'hidden')
+ S
%span
Snippets
+ %li.divider
= nav_link(controller: :help) do
= link_to help_path, title: 'Help' do
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 299dace3406..37429c7cfc0 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -11,19 +11,19 @@
Project
- if project_nav_tab? :files
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases graphs network)) do
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do
= link_to project_files_path(@project), title: 'Repository', class: 'shortcuts-tree' do
%span
Repository
- if project_nav_tab? :container_registry
- = nav_link(controller: %w(container_registry)) do
+ = nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
%span
Registry
- if project_nav_tab? :issues
- = nav_link(controller: [:issues, :labels, :milestones, :boards]) do
+ = nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do
%span
Issues
@@ -31,7 +31,7 @@
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :merge_requests
- = nav_link(controller: :merge_requests) do
+ = nav_link(controller: @project.default_issues_tracker? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
Merge Requests
diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml
index 76440926a2b..3def26342a1 100644
--- a/app/views/notify/project_was_exported_email.html.haml
+++ b/app/views/notify/project_was_exported_email.html.haml
@@ -2,7 +2,7 @@
Project #{@project.name} was exported successfully.
%p
The project export can be downloaded from:
- = link_to download_export_namespace_project_url(@project.namespace, @project), rel: 'nofollow', download: '', do
+ = link_to download_export_namespace_project_url(@project.namespace, @project), rel: 'nofollow', download: '' do
= @project.name_with_namespace + " export"
%p
The download link will expire in 24 hours.
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index aa9b852035e..9aafff343f0 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -25,11 +25,10 @@
#blob-content-holder.blob-content-holder
%article.file-holder
= render "projects/blob/header", blob: blob
- - if current_user
- .js-file-fork-suggestion-section.file-fork-suggestion.hidden
- %span.file-fork-suggestion-note
- You don't have permission to edit this file. Try forking this project to edit the file.
- = link_to 'Fork', fork_path, method: :post, class: 'btn btn-grouped btn-inverted btn-new'
- %button.js-cancel-fork-suggestion.btn.btn-grouped{ type: 'button' }
- Cancel
- = render blob.to_partial_path(@project), blob: blob
+
+ - if blob.empty?
+ .file-content.code
+ .nothing-here-block
+ Empty file
+ - else
+ = render blob.to_partial_path(@project), blob: blob
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 6c7d389e707..d46e4534497 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -13,7 +13,7 @@
.file-actions.hidden-xs
.btn-group{ role: "group" }<
- = copy_blob_content_button(blob) if !blame && blob_text_viewable?(blob)
+ = copy_blob_content_button(blob) if !blame && blob_rendered_as_text?(blob)
= open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id))
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
@@ -38,3 +38,15 @@
- if current_user
= replace_blob_link
= delete_blob_link
+
+- if current_user
+ .js-file-fork-suggestion-section.file-fork-suggestion.hidden
+ %span.file-fork-suggestion-note
+ You're not allowed to
+ %span.js-file-fork-suggestion-section-action
+ edit
+ files in this project directly. Please fork this project,
+ make your changes there, and submit a merge request.
+ = link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button btn btn-grouped btn-inverted btn-new'
+ %button.js-cancel-fork-suggestion-button.btn.btn-grouped{ type: 'button' }
+ Cancel
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index ea3cecb86a9..73877d730f5 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -1,15 +1,2 @@
.file-content.image_file
- - if blob.svg?
- - if blob.size_within_svg_limits?
- -# We need to scrub SVG but we cannot do so in the RawController: it would
- -# be wrong/strange if RawController modified the data.
- - blob.load_all_data!(@repository)
- - blob = sanitize_svg(blob)
- %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: "#{blob.name}" }
- - else
- .nothing-here-block
- The SVG could not be displayed as it is too large, you can
- #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')}
- instead.
- - else
- %img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" }
+ %img{ src: namespace_project_raw_path(@project.namespace, @project, @id), alt: blob.name }
diff --git a/app/views/projects/blob/_markup.html.haml b/app/views/projects/blob/_markup.html.haml
new file mode 100644
index 00000000000..4ee4b03ff04
--- /dev/null
+++ b/app/views/projects/blob/_markup.html.haml
@@ -0,0 +1,4 @@
+- blob.load_all_data!(@repository)
+
+.file-content.wiki
+ = render_markup(blob.name, blob.data)
diff --git a/app/views/projects/blob/_svg.html.haml b/app/views/projects/blob/_svg.html.haml
new file mode 100644
index 00000000000..93be58fc658
--- /dev/null
+++ b/app/views/projects/blob/_svg.html.haml
@@ -0,0 +1,9 @@
+- if blob.size_within_svg_limits?
+ -# We need to scrub SVG but we cannot do so in the RawController: it would
+ -# be wrong/strange if RawController modified the data.
+ - blob.load_all_data!(@repository)
+ - blob = sanitize_svg(blob)
+ .file-content.image_file
+ %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: blob.name }
+- else
+ = render 'too_large'
diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml
index 7b16d266982..20638f6961d 100644
--- a/app/views/projects/blob/_text.html.haml
+++ b/app/views/projects/blob/_text.html.haml
@@ -1,19 +1,2 @@
-- if blob.only_display_raw?
- .file-content.code
- .nothing-here-block
- File too large, you can
- = succeed '.' do
- = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer'
-
-- else
- - blob.load_all_data!(@repository)
-
- - if blob.empty?
- .file-content.code
- .nothing-here-block Empty file
- - else
- - if markup?(blob.name)
- .file-content.wiki
- = render_markup(blob.name, blob.data)
- - else
- = render 'shared/file_highlight', blob: blob, repository: @repository
+- blob.load_all_data!(@repository)
+= render 'shared/file_highlight', blob: blob, repository: @repository
diff --git a/app/views/projects/blob/_too_large.html.haml b/app/views/projects/blob/_too_large.html.haml
new file mode 100644
index 00000000000..a505f87df40
--- /dev/null
+++ b/app/views/projects/blob/_too_large.html.haml
@@ -0,0 +1,5 @@
+.file-content.code
+ .nothing-here-block
+ The file could not be displayed as it is too large, you can
+ #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')}
+ instead.
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 4b26f944733..4af62461151 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -9,7 +9,7 @@
- if @conflict
.alert.alert-danger
Someone edited the file the same time you did. Please check out
- = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer'
+ = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@branch_name, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs.
.editor-title-row
%h3.page-title.blob-edit-page-title
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index b6738c3380f..b9b3f3ec7a3 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -8,7 +8,7 @@
#tree-holder.tree-holder
= render 'blob', blob: @blob
- - if can_edit_blob?(@blob)
+ - if can_modify_blob?(@blob)
= render 'projects/blob/remove'
- title = "Replace #{@blob.name}"
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index bd1f2d96f56..91b86280e4c 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -15,16 +15,14 @@
.dropdown.inline>
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light
- = projects_sort_options_hash[@sort]
+ = branches_sort_options_hash[@sort]
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- %li
- = link_to filter_branches_path(sort: sort_value_name) do
- = sort_title_name
- = link_to filter_branches_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to filter_branches_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ Sort by
+ - branches_sort_options_hash.each do |value, title|
+ %li
+ = link_to title, filter_branches_path(sort: value), class: ("is-active" if @sort == value)
- if can? current_user, :push_code, @project
= link_to namespace_project_merged_branches_path(@project.namespace, @project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index f4a66398c85..c4159ce1a36 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -136,7 +136,7 @@
- else
= build.id
- if build.retried?
- %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+ %i.fa.fa-spinner.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
:javascript
new Sidebar();
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 0faad57a312..7cb2ec83cc7 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -71,11 +71,11 @@
= custom_icon('scroll_down_hover_active')
#up-build-trace
%pre.build-trace#build-trace
- .js-truncated-info.truncated-info.hidden
- %span<
- Showing last
- %span.js-truncated-info-size><
- KiB of log
+ .js-truncated-info.truncated-info.hidden<
+ Showing last
+ %span.js-truncated-info-size.truncated-info-size><
+ KiB of log -
+ %a.js-raw-link.raw-link{ :href => raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 4700b7a9a45..2c3fd1fcd4d 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -36,7 +36,7 @@
= icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.')
- if retried
- = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried')
+ = icon('spinner', class: 'text-warning has-tooltip', title: 'Job was retried')
.label-container
- if job.tags.any?
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index 5c38b5ad9c0..438a98c3e95 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -4,7 +4,7 @@
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
- elsif blob.only_display_raw?
- .nothing-here-block This file is too large to display.
+ .nothing-here-block The file could not be displayed because it is too large.
- elsif blob_text_viewable?(blob)
- if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index f920f359de2..45c95f7ab6a 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -5,8 +5,7 @@
- left = line[:left]
- right = line[:right]
- last_line = right.new_pos if right
- - unless @diff_notes_disabled
- - discussions_left, discussions_right = parallel_diff_discussions(left, right, diff_file)
+ - discussions_left, discussions_right = parallel_diff_discussions(left, right, diff_file)
%tr.line_holder.parallel
- if left
- case left.type
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index ebd1a914ee7..5f3968b6709 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -4,11 +4,10 @@
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
%table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
- - discussions = @grouped_diff_discussions unless @diff_notes_disabled
= render partial: "projects/diffs/line",
collection: diff_file.highlighted_diff_lines,
as: :line,
- locals: { diff_file: diff_file, discussions: discussions }
+ locals: { diff_file: diff_file, discussions: @grouped_diff_discussions }
- if !diff_file.new_file && !diff_file.deleted_file && diff_file.highlighted_diff_lines.any?
- last_line = diff_file.highlighted_diff_lines.last
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index 2e54af698aa..766f119116f 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -13,9 +13,6 @@
Environment:
= link_to @environment.name, environment_path(@environment)
- .col-sm-6
- .nav-controls
- = render 'projects/deployments/actions', deployment: @environment.last_deployment
.prometheus-state
.js-getting-started.hidden
.row
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 885795ccb5c..fcbd8829595 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -79,4 +79,5 @@
= render 'shared/issuable/sidebar', issuable: @issue
+= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('issue_show')
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index a80a07b52e6..7f0059cdcda 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
- page_title "Edit", @label.name, "Labels"
-= render "projects/issues/head"
+= render "shared/mr_head"
%div{ class: container_class }
%h3.page-title
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 29f861c09c6..fc72c4fb635 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,7 +1,7 @@
- @no_container = true
- page_title "Labels"
- hide_class = ''
-= render "projects/issues/head"
+= render "shared/mr_head"
- if @labels.exists? || @prioritized_labels.exists?
%div{ class: container_class }
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index f0d9be744d1..8f6c085a361 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
- page_title "New Label"
-= render "projects/issues/head"
+= render "shared/mr_head"
%div{ class: container_class }
%h3.page-title
diff --git a/app/views/projects/merge_requests/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml
new file mode 100644
index 00000000000..b7f73fe5339
--- /dev/null
+++ b/app/views/projects/merge_requests/_head.html.haml
@@ -0,0 +1,21 @@
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: :merge_requests) do
+ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
+ %span
+ List
+
+ - if project_nav_tab? :labels
+ = nav_link(controller: :labels) do
+ = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
+ %span
+ Labels
+
+ - if project_nav_tab? :milestones
+ = nav_link(controller: :milestones) do
+ = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
+ %span
+ Milestones
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 03069804c86..da79ca2ee75 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -46,7 +46,7 @@
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
- = render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json))
+ = render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json)), disable_initialization: true
.mr-loading-status
= spinner
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 64f17ab34b1..6bf0035e051 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -2,6 +2,9 @@
- @bulk_edit = can?(current_user, :admin_merge_request, @project)
- page_title "Merge Requests"
+- unless @project.default_issues_tracker?
+ = content_for :sub_nav do
+ = render "projects/merge_requests/head"
= render 'projects/last_push'
- content_for :page_specific_javascripts do
diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml
index de4aa255bbd..2f1dbe87619 100644
--- a/app/views/projects/merge_requests/show/_pipelines.html.haml
+++ b/app/views/projects/merge_requests/show/_pipelines.html.haml
@@ -1,3 +1,4 @@
- endpoint_path = local_assigns[:endpoint] || pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :json)
+- disable_initialization = local_assigns.fetch(:disable_initialization, false)
-= render 'projects/commit/pipelines_list', endpoint: endpoint_path
+= render 'projects/commit/pipelines_list', endpoint: endpoint_path, disable_initialization: disable_initialization
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 74a7b1dc498..547be78992e 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -72,13 +72,16 @@
= link_to namespace_project_compare_path(@project.namespace, @project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
new commits
from
- %code= @merge_request.target_branch
+ = succeed '.' do
+ %code= @merge_request.target_branch
- - unless @merge_request_diff.latest? && !@start_sha
+ - if @diff_notes_disabled
.comments-disabled-notif.content-block
= icon('info-circle')
- if @start_sha
Comments are disabled because you're comparing two versions of this merge request.
- else
- Comments are disabled because you're viewing an old version of this merge request.
- = link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm'
+ Discussions on this version of the merge request are displayed but comment creation is disabled.
+
+ .pull-right
+ = link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm'
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index bc426f1dc0c..0872a1a0503 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -19,6 +19,8 @@
= render 'projects/merge_requests/widget/open/conflicts'
- elsif @merge_request.work_in_progress?
= render 'projects/merge_requests/widget/open/wip'
+ - elsif @merge_request.merge_when_pipeline_succeeds? && @merge_request.merge_error.present?
+ = render 'projects/merge_requests/widget/open/error'
- elsif @merge_request.merge_when_pipeline_succeeds?
= render 'projects/merge_requests/widget/open/merge_when_pipeline_succeeds'
- elsif !@merge_request.can_be_merged_by?(current_user)
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index cb117d1908c..4cbd22150c7 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -18,7 +18,7 @@
Select merge moment
%ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
%li
- = link_to "#", class: "merge_when_pipeline_succeeds" do
+ = link_to "#", class: "merge-when-pipeline-succeeds" do
= icon('check fw')
Merge when pipeline succeeds
%li
diff --git a/app/views/projects/merge_requests/widget/open/_error.html.haml b/app/views/projects/merge_requests/widget/open/_error.html.haml
new file mode 100644
index 00000000000..bbdc053609f
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/open/_error.html.haml
@@ -0,0 +1,6 @@
+%h4
+ = icon('exclamation-triangle')
+ This merge request failed to be merged automatically
+
+%p
+ = @merge_request.merge_error
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index 55b0b837c6d..1e66c6079e3 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,11 +1,11 @@
- @no_container = true
- page_title "Edit", @milestone.title, "Milestones"
-= render "projects/issues/head"
+= render "shared/mr_head"
%div{ class: container_class }
%h3.page-title
- Edit Milestone #{@milestone.to_reference}
+ Edit Milestone
%hr
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 8e85b2e8a20..e1096bd1d67 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
- page_title 'Milestones'
-= render 'projects/issues/head'
+= render "shared/mr_head"
%div{ class: container_class }
.top-area
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index cda093ade81..586eb909afa 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
- page_title "New Milestone"
-= render "projects/issues/head"
+= render "shared/mr_head"
%div{ class: container_class }
%h3.page-title
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 8b62b156853..e8c9d7f8429 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,7 +1,7 @@
- @no_container = true
- page_title @milestone.title, "Milestones"
- page_description @milestone.description
-= render "projects/issues/head"
+= render "shared/mr_head"
%div{ class: container_class }
.detail-page-header.milestone-page-header
@@ -17,7 +17,7 @@
.header-text-content
%span.identifier
%strong
- Milestone #{@milestone.to_reference}
+ Milestone
- if @milestone.due_date || @milestone.start_date
= milestone_date_range(@milestone)
.milestone-buttons
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 0c7b53e5a9a..9e292729425 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -145,7 +145,8 @@
}
});
+ $('#project_import_url').disable();
$('.import_git').click(function( event ) {
- $projectImportUrl = $('#project_import_url')
- $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'))
+ $projectImportUrl = $('#project_import_url');
+ $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
});
diff --git a/app/views/projects/notes/_comment_button.html.haml b/app/views/projects/notes/_comment_button.html.haml
index 6bb55f04b6e..29cf5825292 100644
--- a/app/views/projects/notes/_comment_button.html.haml
+++ b/app/views/projects/notes/_comment_button.html.haml
@@ -16,7 +16,7 @@
%p
Add a general comment to this #{noteable_name}.
- %li.divider
+ %li.divider.droplab-item-ignore
%li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => 'Start discussion', 'close-text' => "Start discussion & close #{noteable_name}", 'reopen-text' => "Start discussion & reopen #{noteable_name}" } }
%a{ href: '#' }
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index c12c05eeb73..7cf604bb772 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -12,19 +12,21 @@
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
- %a.visible-xs{ href: user_path(note.author) }
- = note.author.to_reference
- = link_to_member(note.project, note.author, avatar: false, extra_class: 'hidden-xs')
- .note-headline-light
- %span.hidden-xs
- = note.author.to_reference
- - unless note.system
- commented
- - if note.system
- %span.system-note-message
- = note.redacted_note_html
- %a{ href: "##{dom_id(note)}" }
- = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
+ .note-header-info
+ %a{ href: user_path(note.author) }
+ %span.hidden-xs
+ = sanitize(note.author.name)
+ %span.note-headline-light
+ = note.author.to_reference
+ %span.note-headline-light
+ %span.note-headline-meta
+ - unless note.system
+ commented
+ - if note.system
+ %span.system-note-message
+ = note.redacted_note_html
+ %a{ href: "##{dom_id(note)}" }
+ = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
.note-actions
- access = note_max_access_for_user(note)
@@ -52,15 +54,15 @@
":aria-label" => "buttonText",
"@click" => "resolve",
":title" => "buttonText",
- "v-show" => "!loading",
":ref" => "'button'" }
- = icon("spin spinner", "v-show" => "loading")
- = render "shared/icons/icon_status_success.svg"
+ = icon("spin spinner", "v-show" => "loading", class: 'loading')
+ %div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg"
- if current_user
- if note.emoji_awardable?
- = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
+ - user_authored = note.user_authored?(current_user)
+ = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley')
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 3d73284699f..38237d2d97d 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -17,4 +17,4 @@
"ci-lint-path" => ci_lint_path } }
= page_specific_javascript_bundle_tag('common_vue')
-= page_specific_javascript_bundle_tag('vue_pipelines')
+= page_specific_javascript_bundle_tag('pipelines')
diff --git a/app/views/projects/registry/repositories/_image.html.haml b/app/views/projects/registry/repositories/_image.html.haml
index d183ce34a3a..8bc78f8d018 100644
--- a/app/views/projects/registry/repositories/_image.html.haml
+++ b/app/views/projects/registry/repositories/_image.html.haml
@@ -4,7 +4,7 @@
= icon('chevron-down', 'aria-hidden': 'true')
= escape_once(image.path)
- = clipboard_button(clipboard_text: "docker pull #{image.path}")
+ = clipboard_button(clipboard_text: "docker pull #{image.location}")
.controls.hidden-xs.pull-right
= link_to namespace_project_container_registry_path(@project.namespace, @project, image),
diff --git a/app/views/projects/registry/repositories/_tag.html.haml b/app/views/projects/registry/repositories/_tag.html.haml
index 854b7d0ebf7..378a23f07e6 100644
--- a/app/views/projects/registry/repositories/_tag.html.haml
+++ b/app/views/projects/registry/repositories/_tag.html.haml
@@ -1,7 +1,7 @@
%tr.tag
%td
= escape_once(tag.name)
- = clipboard_button(text: "docker pull #{tag.path}")
+ = clipboard_button(text: "docker pull #{tag.location}")
%td
- if tag.revision
%span.has-tooltip{ title: "#{tag.revision}" }
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index edfe6da1816..d6c4195e2d0 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -12,8 +12,8 @@
= render "projects/last_push"
= render "home_panel"
-- if current_user && can?(current_user, :download_code, @project)
- %nav.project-stats.limit-container-width{ class: container_class }
+- if can?(current_user, :download_code, @project)
+ %nav.project-stats{ class: container_class }
%ul.nav
%li
= link_to project_files_path(@project) do
@@ -70,15 +70,15 @@
= link_to 'Set up Koding', add_koding_stack_path(@project)
- if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
%li.missing
- = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do
+ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do
Set up auto deploy
- if @repository.commit
- .limit-container-width{ class: container_class }
+ %div{ class: container_class }
.project-last-commit
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
-.limit-container-width{ class: container_class }
+%div{ class: container_class }
- if @project.archived?
.text-warning.center.prepend-top-20
%p
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index fb39028529d..24b92094b7d 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", @snippet.title, "Snippets"
+- page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
%h3.page-title
Edit Snippet
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index e35385f4cab..7c6be003d4c 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,4 +1,4 @@
-- page_title @snippet.title, "Snippets"
+- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
= render 'shared/snippets/header'
diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml
index 8582bcbb8cc..70d654fa9a0 100644
--- a/app/views/projects/triggers/_form.html.haml
+++ b/app/views/projects/triggers/_form.html.haml
@@ -19,7 +19,7 @@
%strong Schedule trigger (experimental)
.help-block
If checked, this trigger will be executed periodically according to cron and timezone.
- = link_to icon('question-circle'), help_page_path('ci/triggers', anchor: 'schedule')
+ = link_to icon('question-circle'), help_page_path('ci/triggers/README', anchor: 'using-scheduled-triggers')
.form-group
= schedule_fields.label :cron, "Cron", class: "label-light"
= schedule_fields.text_field :cron, class: "form-control", title: 'Cron specification is required.', placeholder: "0 1 * * *"
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml
index c7cebf45160..0ce597dcf21 100644
--- a/app/views/projects/variables/_table.html.haml
+++ b/app/views/projects/variables/_table.html.haml
@@ -14,7 +14,7 @@
%tr
%td.variable-key= variable.key
%td.variable-value{ "data-value" => variable.value }******
- %td
+ %td.variable-menu
= link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do
%span.sr-only
Update
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 86178257af8..6a578dbf640 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -5,5 +5,5 @@
= link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
Page history
- if can?(current_user, :create_wiki, @project) && @page.latest?
- = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do
+ = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn js-wiki-edit" do
Edit
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index e010f21de5a..fc4385865a4 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -3,6 +3,8 @@
= confidential_icon(issue)
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title
+ - if issue.closed?
+ %span.label.label-danger.prepend-left-5 Closed
.pull-right ##{issue.iid}
- if issue.description.present?
.description.term
@@ -10,6 +12,3 @@
= search_md_sanitize(issue, :description)
%span.light
#{issue.project.name_with_namespace}
- - if issue.closed?
- .pull-right
- %span.label.label-danger Closed
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 2e6adf3027c..9b583285d02 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -2,6 +2,10 @@
%h4
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%span.term.str-truncated= merge_request.title
+ - if merge_request.merged?
+ %span.label.label-primary.prepend-left-5 Merged
+ - elsif merge_request.closed?
+ %span.label.label-danger.prepend-left-5 Closed
.pull-right= merge_request.to_reference
- if merge_request.description.present?
.description.term
@@ -9,8 +13,3 @@
= search_md_sanitize(merge_request, :description)
%span.light
#{merge_request.project.name_with_namespace}
- .pull-right
- - if merge_request.merged?
- %span.label.label-primary Merged
- - elsif merge_request.closed?
- %span.label.label-danger Closed
diff --git a/app/views/shared/_branch_switcher.html.haml b/app/views/shared/_branch_switcher.html.haml
index 7799aff6b5b..69e3f3042a9 100644
--- a/app/views/shared/_branch_switcher.html.haml
+++ b/app/views/shared/_branch_switcher.html.haml
@@ -1,8 +1,8 @@
-- dropdown_toggle_text = @target_branch || tree_edit_branch
-= hidden_field_tag 'target_branch', dropdown_toggle_text
+- dropdown_toggle_text = @branch_name || tree_edit_branch
+= hidden_field_tag 'branch_name', dropdown_toggle_text
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'target_branch', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' }
+ = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'branch_name', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' }
.dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging.dropdown-menu-branches
= render partial: 'shared/projects/blob/branch_page_default'
= render partial: 'shared/projects/blob/branch_page_create'
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 54b5ae2402e..1c7c73be933 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -2,7 +2,7 @@
= f.label :import_url, class: 'control-label' do
%span Git repository URL
.col-sm-10
- = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true
+ = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
.well.prepend-top-20
%ul
diff --git a/app/views/shared/_mr_head.html.haml b/app/views/shared/_mr_head.html.haml
new file mode 100644
index 00000000000..4211ec6351d
--- /dev/null
+++ b/app/views/shared/_mr_head.html.haml
@@ -0,0 +1,4 @@
+- if @project.default_issues_tracker?
+ = render "projects/issues/head"
+- else
+ = render "projects/merge_requests/head"
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 3ac5e15d1c4..0b37fe3013b 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -1,11 +1,11 @@
= render 'shared/commit_message_container', placeholder: placeholder
- if @project.empty_repo?
- = hidden_field_tag 'target_branch', @ref
+ = hidden_field_tag 'branch_name', @ref
- else
- if can?(current_user, :push_code, @project)
.form-group.branch
- = label_tag 'target_branch', 'Target branch', class: 'control-label'
+ = label_tag 'branch_name', 'Target branch', class: 'control-label'
.col-sm-10
= render 'shared/branch_switcher'
@@ -16,7 +16,7 @@
= check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
Start a <strong>new merge request</strong> with these changes
- else
- = hidden_field_tag 'target_branch', @target_branch || tree_edit_branch
+ = hidden_field_tag 'branch_name', @branch_name || tree_edit_branch
= hidden_field_tag 'create_merge_request', 1
= hidden_field_tag 'original_branch', @ref, class: 'js-original-branch'
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index e8062848fc3..b20055a564e 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -30,9 +30,10 @@
new Pikaday({
field: $dateField.get(0),
- theme: 'gitlab-theme',
+ theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
minDate: new Date(),
+ container: $dateField.parent().get(0),
onSelect: function(dateText) {
$dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml
index 8f1293adcb1..8308baa7829 100644
--- a/app/views/shared/_user_callout.html.haml
+++ b/app/views/shared/_user_callout.html.haml
@@ -3,12 +3,11 @@
%button.btn.btn-default.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss customize experience box' }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
- .row
- .col-sm-3.col-xs-12.svg-container
- = custom_icon('icon_customization')
- .col-sm-8.col-xs-12.inner-content
- %h4
- Customize your experience
- %p
- Change syntax themes, default project pages, and more in preferences.
- = link_to 'Check it out', profile_preferences_path, class: 'btn btn-default js-close-callout'
+ .svg-container
+ = custom_icon('icon_customization')
+ .user-callout-copy
+ %h4
+ Customize your experience
+ %p
+ Change syntax themes, default project pages, and more in preferences.
+ = link_to 'Check it out', profile_preferences_path, class: 'btn btn-primary js-close-callout'
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 7a7e3d46796..c229d18903f 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -16,6 +16,8 @@
Also, issues are searchable and filterable.
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
+ = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
- else
- %h4 There are no issues to show.
- = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
+ .text-center
+ %h4 There are no issues to show.
+ = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 09f946f1d88..b361ec86ced 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -27,7 +27,8 @@
= visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40
- = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ = link_to group do
+ = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group_name, group, class: 'group-name'
diff --git a/app/views/shared/icons/_icon_arrow_circle_o_right.svg b/app/views/shared/icons/_icon_arrow_circle_o_right.svg
index db28b5e2d7a..5e45c6c15ce 100644
--- a/app/views/shared/icons/_icon_arrow_circle_o_right.svg
+++ b/app/views/shared/icons/_icon_arrow_circle_o_right.svg
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1280 896q0 14-9 23l-320 320q-9 9-23 9-13 0-22.5-9.5t-9.5-22.5v-192h-352q-13 0-22.5-9.5t-9.5-22.5v-192q0-13 9.5-22.5t22.5-9.5h352v-192q0-14 9-23t23-9q12 0 24 10l319 319q9 9 9 23zm160 0q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><g fill-rule="evenodd"><path fill-rule="nonzero" d="m0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7m1 0c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6"/><path d="m7 6h-2.702c-.154 0-.298.132-.298.295v1.41c0 .164.133.295.298.295h2.702v1.694c0 .18.095.209.213.09l2.539-2.568c.115-.116.118-.312 0-.432l-2.539-2.568c-.115-.116-.213-.079-.213.09v1.694"/></g></svg>
diff --git a/app/views/shared/icons/_icon_merged.svg b/app/views/shared/icons/_icon_merged.svg
index d8f96558bea..43d591daefa 100644
--- a/app/views/shared/icons/_icon_merged.svg
+++ b/app/views/shared/icons/_icon_merged.svg
@@ -1 +1 @@
-<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M9.427 6.523a.932.932 0 0 0-.808.489v-.01c-.49-.01-1.059-.172-1.46-.489-.35-.278-.7-.772-.882-1.17a.964.964 0 0 0 .35-.744.943.943 0 0 0-.934-.959c-.518 0-.933.432-.933.964 0 .35.191.662.467.825v3.147a.97.97 0 0 0-.467.825c0 .532.415.959.933.959a.943.943 0 0 0 .934-.96.965.965 0 0 0-.467-.824V6.844c.313.336.672.61 1.073.81.402.202.948.303 1.386.308v-.01c.168.293.467.49.808.49a.943.943 0 0 0 .933-.96.943.943 0 0 0-.933-.96z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m2 3c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1m.761.85c.154 2.556 1.987 4.692 4.45 5.255.328-.655 1.01-1.105 1.789-1.105 1.105 0 2 .895 2 2 0 1.105-.895 2-2 2-.89 0-1.645-.582-1.904-1.386-1.916-.376-3.548-1.5-4.596-3.044v4.493c.863.222 1.5 1.01 1.5 1.937 0 1.105-.895 2-2 2-1.105 0-2-.895-2-2 0-.74.402-1.387 1-1.732v-8.535c-.598-.346-1-.992-1-1.732 0-1.105.895-2 2-2 1.105 0 2 .895 2 2 0 .835-.512 1.551-1.239 1.85m6.239 7.15c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1m-7 4c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1" transform="translate(3)"/></svg>
diff --git a/app/views/shared/icons/_trash_o.svg b/app/views/shared/icons/_icon_trash_o.svg
index 0d7a91ab536..0d7a91ab536 100644
--- a/app/views/shared/icons/_trash_o.svg
+++ b/app/views/shared/icons/_icon_trash_o.svg
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index b447996a8ab..f1350169bbe 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -12,13 +12,14 @@
class: "check_all_issues left"
.issues-other-filters.filtered-search-wrapper
.filtered-search-box
- = dropdown_tag(content_tag(:i, '', class: 'fa fa-history'),
- options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
- toggle_class: "filtered-search-history-dropdown-toggle-button",
- dropdown_class: "filtered-search-history-dropdown",
- content_class: "filtered-search-history-dropdown-content",
- title: "Recent searches" }) do
- .js-filtered-search-history-dropdown
+ - if type != :boards_modal && type != :boards
+ = dropdown_tag(content_tag(:i, '', class: 'fa fa-history'),
+ options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
+ toggle_class: "filtered-search-history-dropdown-toggle-button",
+ dropdown_class: "filtered-search-history-dropdown",
+ content_class: "filtered-search-history-dropdown-content",
+ title: "Recent searches" }) do
+ .js-filtered-search-history-dropdown
.filtered-search-box-input-container
.scroll-container
%ul.tokens-container.list-unstyled
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index ed94773ef89..a74cdbe274b 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -3,10 +3,10 @@
= f.label :start_date, "Start Date", class: "control-label"
.col-sm-10
= f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date"
- %a.inline.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
+ %a.inline.pull-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
.col-md-6
.form-group
= f.label :due_date, "Due Date", class: "control-label"
.col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
- %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
+ %a.inline.pull-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 761f0b606b5..c3b40433c9a 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -12,10 +12,11 @@
= cache(cache_key) do
- if avatar
.avatar-container.s40
- - if use_creator_avatar
- = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- - else
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ = link_to project_path(project), class: dom_class(project) do
+ - if use_creator_avatar
+ = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
+ - else
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
.project-details
%h3.prepend-top-0.append-bottom-0
= link_to project_path(project), class: dom_class(project) do
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index 915bf98eb3e..18ebeb78f87 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", @snippet.title, "Snippets"
+- page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
%h3.page-title
Edit Snippet
%hr
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index da9fb755a36..e5711ca79c7 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,4 +1,4 @@
-- page_title @snippet.title, "Snippets"
+- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
= render 'shared/snippets/header'
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 969ea7ab9e6..03e5dd97405 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -84,19 +84,19 @@
.fade-right= icon('angle-right')
%ul.nav-links.center.user-profile-nav.scrolling-tabs
%li.js-activity-tab
- = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
+ = link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
Activity
%li.js-groups-tab
- = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
+ = link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
Groups
%li.js-contributed-tab
- = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
+ = link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
Contributed projects
%li.js-projects-tab
- = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
+ = link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
Personal projects
%li.js-snippets-tab
- = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do
+ = link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
Snippets
%div{ class: container_class }
diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb
index def0ab1dde1..f7ae996bb17 100644
--- a/app/workers/build_coverage_worker.rb
+++ b/app/workers/build_coverage_worker.rb
@@ -3,7 +3,6 @@ class BuildCoverageWorker
include BuildQueue
def perform(build_id)
- Ci::Build.find_by(id: build_id)
- .try(:update_coverage)
+ Ci::Build.find_by(id: build_id)&.update_coverage
end
end
diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb
deleted file mode 100644
index c4cb4733482..00000000000
--- a/app/workers/clear_database_cache_worker.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# This worker clears all cache fields in the database, working in batches.
-class ClearDatabaseCacheWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
-
- BATCH_SIZE = 1000
-
- def perform
- CacheMarkdownField.caching_classes.each do |kls|
- fields = kls.cached_markdown_fields.html_fields
- clear_cache_fields = fields.each_with_object({}) do |field, memo|
- memo[field] = nil
- end
-
- Rails.logger.debug("Clearing Markdown cache for #{kls}: #{fields.inspect}")
-
- kls.unscoped.in_batches(of: BATCH_SIZE) do |relation|
- relation.update_all(clear_cache_fields)
- end
- end
-
- nil
- end
-end
diff --git a/app/workers/gitlab_usage_ping_worker.rb b/app/workers/gitlab_usage_ping_worker.rb
new file mode 100644
index 00000000000..2f02235b0ac
--- /dev/null
+++ b/app/workers/gitlab_usage_ping_worker.rb
@@ -0,0 +1,31 @@
+class GitlabUsagePingWorker
+ LEASE_TIMEOUT = 86400
+
+ include Sidekiq::Worker
+ include CronjobQueue
+ include HTTParty
+
+ def perform
+ return unless current_application_settings.usage_ping_enabled
+
+ # Multiple Sidekiq workers could run this. We should only do this at most once a day.
+ return unless try_obtain_lease
+
+ begin
+ HTTParty.post(url,
+ body: Gitlab::UsageData.to_json(force_refresh: true),
+ headers: { 'Content-type' => 'application/json' }
+ )
+ rescue HTTParty::Error => e
+ Rails.logger.info "Unable to contact GitLab, Inc.: #{e}"
+ end
+ end
+
+ def try_obtain_lease
+ Gitlab::ExclusiveLease.new('gitlab_usage_ping_worker:ping', timeout: LEASE_TIMEOUT).try_obtain
+ end
+
+ def url
+ 'https://version.gitlab.com/usage_data'
+ end
+end
diff --git a/app/workers/schedule_update_user_activity_worker.rb b/app/workers/schedule_update_user_activity_worker.rb
new file mode 100644
index 00000000000..6c2c3e437f3
--- /dev/null
+++ b/app/workers/schedule_update_user_activity_worker.rb
@@ -0,0 +1,10 @@
+class ScheduleUpdateUserActivityWorker
+ include Sidekiq::Worker
+ include CronjobQueue
+
+ def perform(batch_size = 500)
+ Gitlab::UserActivities.new.each_slice(batch_size) do |batch|
+ UpdateUserActivityWorker.perform_async(Hash[batch])
+ end
+ end
+end
diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb
index baf2f12eeac..55d4e7d6dab 100644
--- a/app/workers/system_hook_worker.rb
+++ b/app/workers/system_hook_worker.rb
@@ -2,6 +2,8 @@ class SystemHookWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ sidekiq_options retry: 4
+
def perform(hook_id, data, hook_name)
SystemHook.find(hook_id).execute(data, hook_name)
end
diff --git a/app/workers/update_user_activity_worker.rb b/app/workers/update_user_activity_worker.rb
new file mode 100644
index 00000000000..b3c2f13aa33
--- /dev/null
+++ b/app/workers/update_user_activity_worker.rb
@@ -0,0 +1,26 @@
+class UpdateUserActivityWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(pairs)
+ pairs = cast_data(pairs)
+ ids = pairs.keys
+ conditions = 'WHEN id = ? THEN ? ' * ids.length
+
+ User.where(id: ids).
+ update_all([
+ "last_activity_on = CASE #{conditions} ELSE last_activity_on END",
+ *pairs.to_a.flatten
+ ])
+
+ Gitlab::UserActivities.new.delete(*ids)
+ end
+
+ private
+
+ def cast_data(pairs)
+ pairs.each_with_object({}) do |(key, value), new_pairs|
+ new_pairs[key.to_i] = Time.at(value.to_i).to_s(:db)
+ end
+ end
+end
diff --git a/changelogs/unreleased/12818-ci-status-as-favicon.yml b/changelogs/unreleased/12818-ci-status-as-favicon.yml
deleted file mode 100644
index 70194178d90..00000000000
--- a/changelogs/unreleased/12818-ci-status-as-favicon.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show CI status as Favicon on Pipelines, Job and MR pages
-merge_request: 10144
-author:
diff --git a/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml b/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml
deleted file mode 100644
index 953009213df..00000000000
--- a/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose CI/CD status API endpoints with Gitlab::Ci::Status facility on pipeline,
- job and merge request for favicon
-merge_request: 9561
-author: dosuken123
diff --git a/changelogs/unreleased/17325-rugged-gem-update.yml b/changelogs/unreleased/17325-rugged-gem-update.yml
deleted file mode 100644
index 7ca619439c4..00000000000
--- a/changelogs/unreleased/17325-rugged-gem-update.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update rugged to 0.25.1.1
-merge_request: 10286
-author: Elan Ruusamäe
diff --git a/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml b/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml
deleted file mode 100644
index fabe24e485a..00000000000
--- a/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Tags can be protected, restricting creation of matching tags by user role
-merge_request: 10356
-author:
diff --git a/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml b/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml
deleted file mode 100644
index 199f1edec8b..00000000000
--- a/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update permalink/blame buttons with line number fragment hash
-merge_request:
-author:
diff --git a/changelogs/unreleased/20841-getting-started-better-empty-state-for-merge-requests-view.yml b/changelogs/unreleased/20841-getting-started-better-empty-state-for-merge-requests-view.yml
deleted file mode 100644
index 34909c06df3..00000000000
--- a/changelogs/unreleased/20841-getting-started-better-empty-state-for-merge-requests-view.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added merge requests empty state
-merge_request: 7342
-author:
diff --git a/changelogs/unreleased/20914-project-home-width.yml b/changelogs/unreleased/20914-project-home-width.yml
deleted file mode 100644
index 323a614f3c8..00000000000
--- a/changelogs/unreleased/20914-project-home-width.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Limit line length for project home page
-merge_request:
-author:
diff --git a/changelogs/unreleased/21451-allow-disable-mr-link.yml b/changelogs/unreleased/21451-allow-disable-mr-link.yml
deleted file mode 100644
index ef99970a7a2..00000000000
--- a/changelogs/unreleased/21451-allow-disable-mr-link.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add ability to disable Merge Request URL on push
-merge_request: 9663
-author: Alex Sanford
diff --git a/changelogs/unreleased/22303-symbolic-in-tree.yml b/changelogs/unreleased/22303-symbolic-in-tree.yml
deleted file mode 100644
index 02444f571d0..00000000000
--- a/changelogs/unreleased/22303-symbolic-in-tree.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix symlink icon in project tree
-merge_request: 9780
-author: mhasbini
diff --git a/changelogs/unreleased/22714-update-all-instances-of-fa-refresh.yml b/changelogs/unreleased/22714-update-all-instances-of-fa-refresh.yml
new file mode 100644
index 00000000000..ad7c011933f
--- /dev/null
+++ b/changelogs/unreleased/22714-update-all-instances-of-fa-refresh.yml
@@ -0,0 +1,4 @@
+---
+title: Update all instances of the old loading icon
+merge_request: 10490
+author: Andrew Torres
diff --git a/changelogs/unreleased/23363-use-strong-params-in-wikis-controller.yml b/changelogs/unreleased/23363-use-strong-params-in-wikis-controller.yml
deleted file mode 100644
index dd342d38fef..00000000000
--- a/changelogs/unreleased/23363-use-strong-params-in-wikis-controller.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update wikis_controller.rb to use strong params
-merge_request:
-author:
diff --git a/changelogs/unreleased/23655-api-group-issues.yml b/changelogs/unreleased/23655-api-group-issues.yml
deleted file mode 100644
index e19e588d09e..00000000000
--- a/changelogs/unreleased/23655-api-group-issues.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix API group/issues default state filter
-merge_request:
-author: Alexander Randa
diff --git a/changelogs/unreleased/23674-simplify-milestone-summary.yml b/changelogs/unreleased/23674-simplify-milestone-summary.yml
deleted file mode 100644
index 7a315c25151..00000000000
--- a/changelogs/unreleased/23674-simplify-milestone-summary.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Move milestone summary content into the sidebar
-merge_request: 10096
-author:
diff --git a/changelogs/unreleased/23862-fix-group-project-count.yml b/changelogs/unreleased/23862-fix-group-project-count.yml
deleted file mode 100644
index 7b2e9f9bfa6..00000000000
--- a/changelogs/unreleased/23862-fix-group-project-count.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adding non_archived scope for counting projects
-merge_request: 8305
-author: Naveen Kumar
diff --git a/changelogs/unreleased/24137-issuable-permalink.yml b/changelogs/unreleased/24137-issuable-permalink.yml
deleted file mode 100644
index bcc6c6957a1..00000000000
--- a/changelogs/unreleased/24137-issuable-permalink.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Link issuable reference to itself in meta-header
-merge_request: 9641
-author: mhasbini
diff --git a/changelogs/unreleased/24166-close-builds-dropdown.yml b/changelogs/unreleased/24166-close-builds-dropdown.yml
deleted file mode 100644
index c57ffed6b45..00000000000
--- a/changelogs/unreleased/24166-close-builds-dropdown.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent builds dropdown to close when the user clicks in a build
-merge_request:
-author:
diff --git a/changelogs/unreleased/24187-set-git-terminal-prompt-env-var-in-initializer.yml b/changelogs/unreleased/24187-set-git-terminal-prompt-env-var-in-initializer.yml
deleted file mode 100644
index 7fe5c8a84af..00000000000
--- a/changelogs/unreleased/24187-set-git-terminal-prompt-env-var-in-initializer.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Set GIT_TERMINAL_PROMPT env variable in initializer
-merge_request: 10372
-author:
diff --git a/changelogs/unreleased/24215-closed-issues-board.yml b/changelogs/unreleased/24215-closed-issues-board.yml
deleted file mode 100644
index 678ec34b274..00000000000
--- a/changelogs/unreleased/24215-closed-issues-board.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display all closed issues in “done” board list
-merge_request:
-author:
diff --git a/changelogs/unreleased/24240-add-monitoring-endpoints.yml b/changelogs/unreleased/24240-add-monitoring-endpoints.yml
deleted file mode 100644
index a22458965fc..00000000000
--- a/changelogs/unreleased/24240-add-monitoring-endpoints.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add /-/readiness /-/liveness and /-/metrics endpoints to track application health
-merge_request: 10416
-author:
diff --git a/changelogs/unreleased/24421-personal-milestone-count-badges.yml b/changelogs/unreleased/24421-personal-milestone-count-badges.yml
deleted file mode 100644
index 8bbc1ed2dde..00000000000
--- a/changelogs/unreleased/24421-personal-milestone-count-badges.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add dashboard and group milestones count badges
-merge_request: 9836
-author: Alex Braha Stoll
diff --git a/changelogs/unreleased/24501-new-file-existing-branch.yml b/changelogs/unreleased/24501-new-file-existing-branch.yml
deleted file mode 100644
index 31c66b2a978..00000000000
--- a/changelogs/unreleased/24501-new-file-existing-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: New file from interface on existing branch
-merge_request: 8427
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/24784-system-notes-meta-data.yml b/changelogs/unreleased/24784-system-notes-meta-data.yml
deleted file mode 100644
index 757ae9e0527..00000000000
--- a/changelogs/unreleased/24784-system-notes-meta-data.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add metadata to system notes
-merge_request: 9964
-author:
diff --git a/changelogs/unreleased/24861-stringify-group-member-details.yml b/changelogs/unreleased/24861-stringify-group-member-details.yml
deleted file mode 100644
index f56a1060862..00000000000
--- a/changelogs/unreleased/24861-stringify-group-member-details.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Hide form inputs for group member without editing rights
-merge_request: 7816
-author:
diff --git a/changelogs/unreleased/25188-polyfill-es-symbol.yml b/changelogs/unreleased/25188-polyfill-es-symbol.yml
deleted file mode 100644
index d0cf36b9ec6..00000000000
--- a/changelogs/unreleased/25188-polyfill-es-symbol.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add ECMAScript polyfills for Symbol and Array.find
-merge_request: 10120
-author:
diff --git a/changelogs/unreleased/25332-make-file-templates-easy-to-use-and-discover.yml b/changelogs/unreleased/25332-make-file-templates-easy-to-use-and-discover.yml
deleted file mode 100644
index fc95858f783..00000000000
--- a/changelogs/unreleased/25332-make-file-templates-easy-to-use-and-discover.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove no-new annotation from file_template_mediator.js.
-merge_request: !9782
-author:
diff --git a/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml b/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml
deleted file mode 100644
index 5b755a8bc32..00000000000
--- a/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Create a new issue for a single discussion in a Merge Request
-merge_request: 8266
-author: Bob Van Landuyt
diff --git a/changelogs/unreleased/25556-prevent-users-from-disconnecting-gitlab-account-from-cas.yml b/changelogs/unreleased/25556-prevent-users-from-disconnecting-gitlab-account-from-cas.yml
deleted file mode 100644
index 17e38ba6243..00000000000
--- a/changelogs/unreleased/25556-prevent-users-from-disconnecting-gitlab-account-from-cas.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent users from disconnecting GitLab account from CAS
-merge_request: 10282
-author:
diff --git a/changelogs/unreleased/26188-tag-creation-404-for-guests.yml b/changelogs/unreleased/26188-tag-creation-404-for-guests.yml
deleted file mode 100644
index fb00d46ea1f..00000000000
--- a/changelogs/unreleased/26188-tag-creation-404-for-guests.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't show links to tag a commit for users that are not permitted
-merge_request: 8407
-author:
diff --git a/changelogs/unreleased/26202-change-dropdown-style-slightly.yml b/changelogs/unreleased/26202-change-dropdown-style-slightly.yml
deleted file mode 100644
index 827224abf5a..00000000000
--- a/changelogs/unreleased/26202-change-dropdown-style-slightly.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Changed dropdown style slightly
-merge_request:
-author:
diff --git a/changelogs/unreleased/26208-animate-drodowns.yml b/changelogs/unreleased/26208-animate-drodowns.yml
new file mode 100644
index 00000000000..580f6c12f67
--- /dev/null
+++ b/changelogs/unreleased/26208-animate-drodowns.yml
@@ -0,0 +1,4 @@
+---
+title: Add animations to all the dropdowns
+merge_request: 8419
+author:
diff --git a/changelogs/unreleased/26236-monospace-gfm.yml b/changelogs/unreleased/26236-monospace-gfm.yml
deleted file mode 100644
index c44f3d4d3dc..00000000000
--- a/changelogs/unreleased/26236-monospace-gfm.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change gfm textarea to use monospace font
-merge_request:
-author:
diff --git a/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml
deleted file mode 100644
index e82cbf00cfb..00000000000
--- a/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Strip reference prefixes on branch creation
-merge_request: 8498
-author: Matthieu Tardy
diff --git a/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml b/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml
deleted file mode 100644
index 44aae486574..00000000000
--- a/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add Undo mark all as done to Todos
-merge_request: 9890
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml b/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml
deleted file mode 100644
index 2e6c10a6bfe..00000000000
--- a/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add Undo to Todos in the Done tab
-merge_request: 8782
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/27174-filter-filters.yml b/changelogs/unreleased/27174-filter-filters.yml
deleted file mode 100644
index 0da1e4d5d3b..00000000000
--- a/changelogs/unreleased/27174-filter-filters.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent filtering issues by multiple Milestones or Authors
-merge_request:
-author:
diff --git a/changelogs/unreleased/27262-issue-recent-searches.yml b/changelogs/unreleased/27262-issue-recent-searches.yml
deleted file mode 100644
index 4bdec5af31d..00000000000
--- a/changelogs/unreleased/27262-issue-recent-searches.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Recent search history for issues
-merge_request:
-author:
diff --git a/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml b/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml
deleted file mode 100644
index 4ea52a70e89..00000000000
--- a/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Include time tracking attributes in webhooks payload
-merge_request: 9942
-author:
diff --git a/changelogs/unreleased/27293-remove-repeated-labels.yml b/changelogs/unreleased/27293-remove-repeated-labels.yml
deleted file mode 100644
index 60caa6e971a..00000000000
--- a/changelogs/unreleased/27293-remove-repeated-labels.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove duplicated tokens in issuable search bar
-merge_request:
-author:
diff --git a/changelogs/unreleased/27503-feature-status-aria-labels.yml b/changelogs/unreleased/27503-feature-status-aria-labels.yml
deleted file mode 100644
index f514fd5b631..00000000000
--- a/changelogs/unreleased/27503-feature-status-aria-labels.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add `aria-label` for feature status accessibility
-merge_request: 9830
-author:
diff --git a/changelogs/unreleased/27574-pipelines-empty-state.yml b/changelogs/unreleased/27574-pipelines-empty-state.yml
deleted file mode 100644
index c26ea97205f..00000000000
--- a/changelogs/unreleased/27574-pipelines-empty-state.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds empty and error state to pipelines
-merge_request:
-author:
diff --git a/changelogs/unreleased/27580-fix-show-go-back.yml b/changelogs/unreleased/27580-fix-show-go-back.yml
deleted file mode 100644
index c7dbbe7a236..00000000000
--- a/changelogs/unreleased/27580-fix-show-go-back.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Shows 'Go Back' link only when browser history is available
-merge_request: 9017
-author:
diff --git a/changelogs/unreleased/27655-clear-emoji-search-after-selection.yml b/changelogs/unreleased/27655-clear-emoji-search-after-selection.yml
new file mode 100644
index 00000000000..5fd02696323
--- /dev/null
+++ b/changelogs/unreleased/27655-clear-emoji-search-after-selection.yml
@@ -0,0 +1,4 @@
+---
+title: Clear emoji search in awards menu after picking emoji
+merge_request:
+author:
diff --git a/changelogs/unreleased/27729-improve-webpack-dev-environment.yml b/changelogs/unreleased/27729-improve-webpack-dev-environment.yml
new file mode 100644
index 00000000000..d04ea70ab1c
--- /dev/null
+++ b/changelogs/unreleased/27729-improve-webpack-dev-environment.yml
@@ -0,0 +1,4 @@
+---
+title: Add webpack_bundle_tag helper to improve non-localhost GDK configurations
+merge_request: 10604
+author:
diff --git a/changelogs/unreleased/27878-new-service-for-creating-user.yml b/changelogs/unreleased/27878-new-service-for-creating-user.yml
deleted file mode 100644
index c07f0cef8db..00000000000
--- a/changelogs/unreleased/27878-new-service-for-creating-user.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Implement user create service
-merge_request: 9220
-author: George Andrinopoulos
diff --git a/changelogs/unreleased/27910-admin-can-create-project-in-all-groups.yml b/changelogs/unreleased/27910-admin-can-create-project-in-all-groups.yml
deleted file mode 100644
index 40fd8dacc82..00000000000
--- a/changelogs/unreleased/27910-admin-can-create-project-in-all-groups.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow admin to view all namespaces
-merge_request:
-author: George Andrinopoulos
diff --git a/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml b/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml
deleted file mode 100644
index c6ba9572f26..00000000000
--- a/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Add `requirements: { id: /.+/ }` for all projects and groups namespaced API
- routes'
-merge_request: 9944
-author:
diff --git a/changelogs/unreleased/28017-separate-ce-params-on-api.yml b/changelogs/unreleased/28017-separate-ce-params-on-api.yml
new file mode 100644
index 00000000000..039a8d207b0
--- /dev/null
+++ b/changelogs/unreleased/28017-separate-ce-params-on-api.yml
@@ -0,0 +1,4 @@
+---
+title: Separate CE params on Grape API
+merge_request:
+author:
diff --git a/changelogs/unreleased/28030-infinite-offset.yml b/changelogs/unreleased/28030-infinite-offset.yml
deleted file mode 100644
index 6f4082d7684..00000000000
--- a/changelogs/unreleased/28030-infinite-offset.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: allow offset query parameter for infinite list pages
-merge_request:
-author:
diff --git a/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml b/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml
deleted file mode 100644
index feca38ff083..00000000000
--- a/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use toggle button to expand / collapse mulit-nested groups
-merge_request: 9501
-author:
diff --git a/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml b/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml
deleted file mode 100644
index dd94b3fe663..00000000000
--- a/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix wrong message on starred projects filtering
-merge_request:
-author: George Andrinopoulos
diff --git a/changelogs/unreleased/28424-labels-support-color-names-in-backend.yml b/changelogs/unreleased/28424-labels-support-color-names-in-backend.yml
deleted file mode 100644
index 00da1e0fa60..00000000000
--- a/changelogs/unreleased/28424-labels-support-color-names-in-backend.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Labels support color names in backend
-merge_request: 9725
-author: Dongqing Hu
diff --git a/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml b/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml
deleted file mode 100644
index 67dbc30e760..00000000000
--- a/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds pipeline mini-graph to system information box in Commit View
-merge_request:
-author:
diff --git a/changelogs/unreleased/28574-jira-trigers.yml b/changelogs/unreleased/28574-jira-trigers.yml
deleted file mode 100644
index 6ebd2c0c2c2..00000000000
--- a/changelogs/unreleased/28574-jira-trigers.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove confusing placeholder for JIRA transition_id
-merge_request:
-author:
diff --git a/changelogs/unreleased/28575-expand-collapse-look.yml b/changelogs/unreleased/28575-expand-collapse-look.yml
new file mode 100644
index 00000000000..d8943316300
--- /dev/null
+++ b/changelogs/unreleased/28575-expand-collapse-look.yml
@@ -0,0 +1,4 @@
+---
+title: Expand/collapse button -> Change to make it look like a toggle
+merge_request: 10720
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/28614-harmonious-color-palette.yml b/changelogs/unreleased/28614-harmonious-color-palette.yml
deleted file mode 100644
index b436e7129a4..00000000000
--- a/changelogs/unreleased/28614-harmonious-color-palette.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update color palette to a more harmonious and consistent one.
-merge_request: 10154
-author:
diff --git a/changelogs/unreleased/28634-todos-margin.yml b/changelogs/unreleased/28634-todos-margin.yml
deleted file mode 100644
index f4221ce4350..00000000000
--- a/changelogs/unreleased/28634-todos-margin.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove extra margin at bottom of todos page
-merge_request:
-author:
diff --git a/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
deleted file mode 100644
index 8b592766bf3..00000000000
--- a/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes dismissable error close is not visible enough
-merge_request: 9516
-author:
diff --git a/changelogs/unreleased/28695-move-all-associated-records-to-ghost-user.yml b/changelogs/unreleased/28695-move-all-associated-records-to-ghost-user.yml
deleted file mode 100644
index c5dcde48028..00000000000
--- a/changelogs/unreleased/28695-move-all-associated-records-to-ghost-user.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Deleting a user should not delete associated records
-merge_request: 10467
-author:
diff --git a/changelogs/unreleased/28713-fe-style-guide.yml b/changelogs/unreleased/28713-fe-style-guide.yml
deleted file mode 100644
index 57edb43e27f..00000000000
--- a/changelogs/unreleased/28713-fe-style-guide.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds Frontend Styleguide to documentation
-merge_request: 9961
-author:
diff --git a/changelogs/unreleased/28732-expandable-folders.yml b/changelogs/unreleased/28732-expandable-folders.yml
deleted file mode 100644
index 9ae30ba6253..00000000000
--- a/changelogs/unreleased/28732-expandable-folders.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add back expandable folder behavior
-merge_request:
-author:
diff --git a/changelogs/unreleased/28799-todo-creation.yml b/changelogs/unreleased/28799-todo-creation.yml
deleted file mode 100644
index c6e05468568..00000000000
--- a/changelogs/unreleased/28799-todo-creation.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Create todos only for new mentions
-merge_request:
-author:
diff --git a/changelogs/unreleased/28810-projectfinder-should-handle-more-options.yml b/changelogs/unreleased/28810-projectfinder-should-handle-more-options.yml
deleted file mode 100644
index e4be16d4b37..00000000000
--- a/changelogs/unreleased/28810-projectfinder-should-handle-more-options.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: ProjectsFinder should handle more options
-merge_request: 9682
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml b/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml
deleted file mode 100644
index 0177394aa0f..00000000000
--- a/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Order milestone issues by position ascending in api
-merge_request: 9635
-author: George Andrinopoulos
diff --git a/changelogs/unreleased/28899-linking-to-edit-file.yml b/changelogs/unreleased/28899-linking-to-edit-file.yml
deleted file mode 100644
index a9f5410693b..00000000000
--- a/changelogs/unreleased/28899-linking-to-edit-file.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Linking to blob edit page handles anonymous users and users without enough permissions
- to edit directly
-merge_request:
-author:
diff --git a/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml b/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml
deleted file mode 100644
index 26989c14958..00000000000
--- a/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: When viewing old wiki page version, edit button should be disabled
-merge_request: 9966
-author: TM Lee
diff --git a/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml b/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml
deleted file mode 100644
index f869249c22b..00000000000
--- a/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix create issue form buttons are misaligned on mobile
-merge_request: 9706
-author: TM Lee
diff --git a/changelogs/unreleased/29043-upgrade-vue-and-remove-warnings.yml b/changelogs/unreleased/29043-upgrade-vue-and-remove-warnings.yml
deleted file mode 100644
index 9055b23a13f..00000000000
--- a/changelogs/unreleased/29043-upgrade-vue-and-remove-warnings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Upgrade VueJS to v2.2.4 and disable dev mode warnings
-merge_request: 9981
-author:
diff --git a/changelogs/unreleased/29046-fix-github-importer-open-prs.yml b/changelogs/unreleased/29046-fix-github-importer-open-prs.yml
deleted file mode 100644
index d279c269f94..00000000000
--- a/changelogs/unreleased/29046-fix-github-importer-open-prs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix GitHub Import deleting branches for open PRs from a fork
-merge_request: 9758
-author:
diff --git a/changelogs/unreleased/29056-backport-ee-cleanup-database-file.yml b/changelogs/unreleased/29056-backport-ee-cleanup-database-file.yml
new file mode 100644
index 00000000000..0ebb9d57611
--- /dev/null
+++ b/changelogs/unreleased/29056-backport-ee-cleanup-database-file.yml
@@ -0,0 +1,4 @@
+---
+title: Turns true value and false value database methods from instance to class methods
+merge_request: 10583
+author:
diff --git a/changelogs/unreleased/29116-maxint-error.yml b/changelogs/unreleased/29116-maxint-error.yml
deleted file mode 100644
index 06e976617d5..00000000000
--- a/changelogs/unreleased/29116-maxint-error.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix projects_limit RangeError on user create
-merge_request:
-author: Alexander Randa
diff --git a/changelogs/unreleased/29128-profile-page-icons.yml b/changelogs/unreleased/29128-profile-page-icons.yml
deleted file mode 100644
index 0215f5c0e8f..00000000000
--- a/changelogs/unreleased/29128-profile-page-icons.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add helpful icons to profile events
-merge_request:
-author:
diff --git a/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml b/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml
deleted file mode 100644
index 0de7754badc..00000000000
--- a/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make authorized projects worker use a specific queue instead of the default one
-merge_request: 9813
-author:
diff --git a/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml b/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml
deleted file mode 100644
index ad0c513f525..00000000000
--- a/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Refactor dropdown_milestone_spec.rb
-merge_request:
-author: George Andrinopoulos
diff --git a/changelogs/unreleased/29189-discussion-button.yml b/changelogs/unreleased/29189-discussion-button.yml
deleted file mode 100644
index eea96362117..00000000000
--- a/changelogs/unreleased/29189-discussion-button.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix alignment of resolve button
-merge_request:
-author:
diff --git a/changelogs/unreleased/29209-sign-up-form-name.yml b/changelogs/unreleased/29209-sign-up-form-name.yml
deleted file mode 100644
index e8e3a71f875..00000000000
--- a/changelogs/unreleased/29209-sign-up-form-name.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change label for name on sign up form
-merge_request:
-author:
diff --git a/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml b/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml
deleted file mode 100644
index dabf9968c5b..00000000000
--- a/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add custom attributes in factories
-merge_request: 9892
-author: George Andrinopoulos
diff --git a/changelogs/unreleased/29341-add-metrics-button-env-overview.yml b/changelogs/unreleased/29341-add-metrics-button-env-overview.yml
deleted file mode 100644
index 16b69235dff..00000000000
--- a/changelogs/unreleased/29341-add-metrics-button-env-overview.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add metrics button to environments overview page
-merge_request: 10234
-author:
diff --git a/changelogs/unreleased/29364-private-projects-mr-fix.yml b/changelogs/unreleased/29364-private-projects-mr-fix.yml
deleted file mode 100644
index ab93d6f337b..00000000000
--- a/changelogs/unreleased/29364-private-projects-mr-fix.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don’t show source project name when user does not have access
-merge_request:
-author:
diff --git a/changelogs/unreleased/29405-fix-project-wiki-update.yml b/changelogs/unreleased/29405-fix-project-wiki-update.yml
deleted file mode 100644
index 85be36f7902..00000000000
--- a/changelogs/unreleased/29405-fix-project-wiki-update.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Project Wiki update
-merge_request: 9990
-author: Dongqing Hu
diff --git a/changelogs/unreleased/29414-fix-toggle-discussion-link-jump.yml b/changelogs/unreleased/29414-fix-toggle-discussion-link-jump.yml
deleted file mode 100644
index 04342f5359d..00000000000
--- a/changelogs/unreleased/29414-fix-toggle-discussion-link-jump.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update toggle buttons to be <button>
-merge_request:
-author:
diff --git a/changelogs/unreleased/29428-new-directory-from-existing-branch.yml b/changelogs/unreleased/29428-new-directory-from-existing-branch.yml
deleted file mode 100644
index b3f7cd1f8f8..00000000000
--- a/changelogs/unreleased/29428-new-directory-from-existing-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: New directory from interface on existing branch
-merge_request: 9921
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/29432-prevent-click-disabled-btn.yml b/changelogs/unreleased/29432-prevent-click-disabled-btn.yml
deleted file mode 100644
index f30570cf68b..00000000000
--- a/changelogs/unreleased/29432-prevent-click-disabled-btn.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix project title validation, prevent clicking on disabled button
-merge_request: 9931
-author:
diff --git a/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml b/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
deleted file mode 100644
index 61ffb64fa8f..00000000000
--- a/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix trigger webhook for ref with a dot
-merge_request: 10001
-author: George Andrinopoulos
diff --git a/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml b/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml
deleted file mode 100644
index 23a32d2c11a..00000000000
--- a/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display full project name with namespace upon deletion
-merge_request:
-author:
diff --git a/changelogs/unreleased/29483-spam-check-only-title-and-description.yml b/changelogs/unreleased/29483-spam-check-only-title-and-description.yml
deleted file mode 100644
index de8cacb250d..00000000000
--- a/changelogs/unreleased/29483-spam-check-only-title-and-description.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Spam check only when spammable attributes have changed
-merge_request:
-author:
diff --git a/changelogs/unreleased/29492-useless-queries.yml b/changelogs/unreleased/29492-useless-queries.yml
deleted file mode 100644
index 266a04be352..00000000000
--- a/changelogs/unreleased/29492-useless-queries.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove useless queries with false conditions (e.g 1=0)
-merge_request: 10141
-author: mhasbini
diff --git a/changelogs/unreleased/29550-fix-quick-submit-on-preview.yml b/changelogs/unreleased/29550-fix-quick-submit-on-preview.yml
deleted file mode 100644
index 71214971ffd..00000000000
--- a/changelogs/unreleased/29550-fix-quick-submit-on-preview.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix quick submit short-cut on preview tab for comments
-merge_request: 10002
-author:
diff --git a/changelogs/unreleased/29555-align-all-todo.yml b/changelogs/unreleased/29555-align-all-todo.yml
deleted file mode 100644
index c1555a96a92..00000000000
--- a/changelogs/unreleased/29555-align-all-todo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: align Mark all as done with other Done buttons on Todos page
-merge_request:
-author:
diff --git a/changelogs/unreleased/29575-polling.yml b/changelogs/unreleased/29575-polling.yml
deleted file mode 100644
index 75016afd455..00000000000
--- a/changelogs/unreleased/29575-polling.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds polling utility function for vue resource
-merge_request:
-author:
diff --git a/changelogs/unreleased/29595-customize-experience-callout.yml b/changelogs/unreleased/29595-customize-experience-callout.yml
new file mode 100644
index 00000000000..ec8393142c6
--- /dev/null
+++ b/changelogs/unreleased/29595-customize-experience-callout.yml
@@ -0,0 +1,4 @@
+---
+title: 29595 Update callout design
+merge_request:
+author:
diff --git a/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml b/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
deleted file mode 100644
index 15d7b9dcafb..00000000000
--- a/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow unauthenticated access to some Branch API GET endpoints
-merge_request:
-author:
diff --git a/changelogs/unreleased/29669-redirect-referer-params.yml b/changelogs/unreleased/29669-redirect-referer-params.yml
deleted file mode 100644
index d8fc7f33049..00000000000
--- a/changelogs/unreleased/29669-redirect-referer-params.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix redirection after login when the referer have params
-merge_request:
-author: mhasbini
diff --git a/changelogs/unreleased/29670-jira-integration-documentation-improvment.yml b/changelogs/unreleased/29670-jira-integration-documentation-improvment.yml
deleted file mode 100644
index 8975f0b6ef3..00000000000
--- a/changelogs/unreleased/29670-jira-integration-documentation-improvment.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added clarification to the Jira integration documentation.
-merge_request: 10066
-author: Matthew Bender
diff --git a/changelogs/unreleased/29734-prometheus-monitoring-page-displays-button-to-control-manual-actions.yml b/changelogs/unreleased/29734-prometheus-monitoring-page-displays-button-to-control-manual-actions.yml
new file mode 100644
index 00000000000..ca4a8889454
--- /dev/null
+++ b/changelogs/unreleased/29734-prometheus-monitoring-page-displays-button-to-control-manual-actions.yml
@@ -0,0 +1,4 @@
+---
+title: Remove pipeline controls for last deployment from Environment monitoring page
+merge_request: 10769
+author:
diff --git a/changelogs/unreleased/29801-add-slash-slack-commands-to-api-doc.yml b/changelogs/unreleased/29801-add-slash-slack-commands-to-api-doc.yml
new file mode 100644
index 00000000000..9c5df690085
--- /dev/null
+++ b/changelogs/unreleased/29801-add-slash-slack-commands-to-api-doc.yml
@@ -0,0 +1,5 @@
+---
+title: Add Slack slash command api to services documentation and rearrange order and
+ cases
+merge_request: 10757
+author: TM Lee
diff --git a/changelogs/unreleased/29816-create-keyboard-shortcut-for-editing-wiki-page.yml b/changelogs/unreleased/29816-create-keyboard-shortcut-for-editing-wiki-page.yml
new file mode 100644
index 00000000000..a165c70a6d3
--- /dev/null
+++ b/changelogs/unreleased/29816-create-keyboard-shortcut-for-editing-wiki-page.yml
@@ -0,0 +1,4 @@
+---
+title: Add keyboard edit shotcut for wiki
+merge_request: 10245
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/29828-change-search-hint-in-new-filters.yml b/changelogs/unreleased/29828-change-search-hint-in-new-filters.yml
deleted file mode 100644
index a9322693ca4..00000000000
--- a/changelogs/unreleased/29828-change-search-hint-in-new-filters.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change hint on first row of filters dropdown to `Press Enter or click to search`
-merge_request: 10138
-author:
diff --git a/changelogs/unreleased/29830-build-scroll-indicator.yml b/changelogs/unreleased/29830-build-scroll-indicator.yml
deleted file mode 100644
index e899a828de7..00000000000
--- a/changelogs/unreleased/29830-build-scroll-indicator.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: fix sidebar padding for build and wiki pages
-merge_request:
-author:
diff --git a/changelogs/unreleased/29843-project-subgroup-transfer.yml b/changelogs/unreleased/29843-project-subgroup-transfer.yml
deleted file mode 100644
index 1cf83517591..00000000000
--- a/changelogs/unreleased/29843-project-subgroup-transfer.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Correctly update paths when changing a child group
-merge_request:
-author:
diff --git a/changelogs/unreleased/29866-navbar-counters.yml b/changelogs/unreleased/29866-navbar-counters.yml
deleted file mode 100644
index c67dff6cffa..00000000000
--- a/changelogs/unreleased/29866-navbar-counters.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add shortcuts and counters to MRs and issues in navbar
-merge_request:
-author:
diff --git a/changelogs/unreleased/2989-run-cicd-pipelines-on-a-schedule-idea1-basic-backend-implementation.yml b/changelogs/unreleased/2989-run-cicd-pipelines-on-a-schedule-idea1-basic-backend-implementation.yml
deleted file mode 100644
index dd56409c35b..00000000000
--- a/changelogs/unreleased/2989-run-cicd-pipelines-on-a-schedule-idea1-basic-backend-implementation.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Resolve "Run CI/CD pipelines on a schedule" - "Basic backend implementation"
-merge_request: 10133
-author: dosuken123
diff --git a/changelogs/unreleased/29897-remove-force-scroll-for-mr-changes-diff.yml b/changelogs/unreleased/29897-remove-force-scroll-for-mr-changes-diff.yml
deleted file mode 100644
index d1da96096f8..00000000000
--- a/changelogs/unreleased/29897-remove-force-scroll-for-mr-changes-diff.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove forced scroll into view when switching to Changes MR tab
-merge_request:
-author:
diff --git a/changelogs/unreleased/29929-jira-doc.yml b/changelogs/unreleased/29929-jira-doc.yml
deleted file mode 100644
index f79dcd84634..00000000000
--- a/changelogs/unreleased/29929-jira-doc.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix link to Jira service documentation
-merge_request:
-author:
diff --git a/changelogs/unreleased/29930-fix-profile-cover-button-a11y.yml b/changelogs/unreleased/29930-fix-profile-cover-button-a11y.yml
deleted file mode 100644
index 754d471c7d7..00000000000
--- a/changelogs/unreleased/29930-fix-profile-cover-button-a11y.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add tooltip and accessibility for profile cover buttons
-merge_request: 10182
-author:
diff --git a/changelogs/unreleased/29950-vue-pagination-icons.yml b/changelogs/unreleased/29950-vue-pagination-icons.yml
deleted file mode 100644
index e03092b8dba..00000000000
--- a/changelogs/unreleased/29950-vue-pagination-icons.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: consistent icons in vue and kaminari pagers
-merge_request:
-author:
diff --git a/changelogs/unreleased/29977-style-comments-and-system-notes-real-time-updates.yml b/changelogs/unreleased/29977-style-comments-and-system-notes-real-time-updates.yml
new file mode 100644
index 00000000000..c1640777e12
--- /dev/null
+++ b/changelogs/unreleased/29977-style-comments-and-system-notes-real-time-updates.yml
@@ -0,0 +1,4 @@
+---
+title: Added quick-update (fade-in) animation to newly rendered notes
+merge_request: 10623
+author:
diff --git a/changelogs/unreleased/30021-api-deploy_keys-can_push-is-not-honoured.yml b/changelogs/unreleased/30021-api-deploy_keys-can_push-is-not-honoured.yml
deleted file mode 100644
index 7584995a11f..00000000000
--- a/changelogs/unreleased/30021-api-deploy_keys-can_push-is-not-honoured.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable creation of deploy keys with write access via the API
-merge_request:
-author:
diff --git a/changelogs/unreleased/30024-owner-can-t-initialize-git-repo-for-new-project-in-group.yml b/changelogs/unreleased/30024-owner-can-t-initialize-git-repo-for-new-project-in-group.yml
deleted file mode 100644
index c43d2732b9a..00000000000
--- a/changelogs/unreleased/30024-owner-can-t-initialize-git-repo-for-new-project-in-group.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Disable invalid service templates
-merge_request:
-author:
diff --git a/changelogs/unreleased/30056-rename-milestones-empty.yml b/changelogs/unreleased/30056-rename-milestones-empty.yml
deleted file mode 100644
index 85db342b6df..00000000000
--- a/changelogs/unreleased/30056-rename-milestones-empty.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Removed Milestone#is_empty?
-merge_request: 10523
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/30112-fix-pipelines-sub-nav-highlight.yml b/changelogs/unreleased/30112-fix-pipelines-sub-nav-highlight.yml
deleted file mode 100644
index deca629be83..00000000000
--- a/changelogs/unreleased/30112-fix-pipelines-sub-nav-highlight.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix sub-nav highlighting for `Environments` and `Jobs` pages
-merge_request: 10254
-author:
diff --git a/changelogs/unreleased/30125-markdown-security.yml b/changelogs/unreleased/30125-markdown-security.yml
deleted file mode 100644
index b766caf7d08..00000000000
--- a/changelogs/unreleased/30125-markdown-security.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove the class attribute from the whitelist for HTML generated from Markdown.
-merge_request:
-author:
diff --git a/changelogs/unreleased/30195-document-search-param-on-api.yml b/changelogs/unreleased/30195-document-search-param-on-api.yml
deleted file mode 100644
index f19f6ab699e..00000000000
--- a/changelogs/unreleased/30195-document-search-param-on-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add search optional param and docs for V4
-merge_request:
-author:
diff --git a/changelogs/unreleased/30291-reopen-mr.yml b/changelogs/unreleased/30291-reopen-mr.yml
deleted file mode 100644
index 4ae3e90eeba..00000000000
--- a/changelogs/unreleased/30291-reopen-mr.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Include reopened MRs when searching for opened ones
-merge_request: 10407
-author:
diff --git a/changelogs/unreleased/30306-transaction-while-moving-issues-to-ghost-user.yml b/changelogs/unreleased/30306-transaction-while-moving-issues-to-ghost-user.yml
new file mode 100644
index 00000000000..5fc57e44be6
--- /dev/null
+++ b/changelogs/unreleased/30306-transaction-while-moving-issues-to-ghost-user.yml
@@ -0,0 +1,4 @@
+---
+title: Add a transaction around move_issues_to_ghost_user
+merge_request: 10465
+author:
diff --git a/changelogs/unreleased/30349-create-users-build-service.yml b/changelogs/unreleased/30349-create-users-build-service.yml
new file mode 100644
index 00000000000..49b571f5646
--- /dev/null
+++ b/changelogs/unreleased/30349-create-users-build-service.yml
@@ -0,0 +1,4 @@
+---
+title: Implement Users::BuildService
+merge_request: 30349
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/30400-fix-blob-highlighting-in-search.yml b/changelogs/unreleased/30400-fix-blob-highlighting-in-search.yml
deleted file mode 100644
index 942258450c0..00000000000
--- a/changelogs/unreleased/30400-fix-blob-highlighting-in-search.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix blob highlighting in search
-merge_request: 10420
-author:
diff --git a/changelogs/unreleased/30484-profile-dropdown-account-name.yml b/changelogs/unreleased/30484-profile-dropdown-account-name.yml
new file mode 100644
index 00000000000..71aa1ce139b
--- /dev/null
+++ b/changelogs/unreleased/30484-profile-dropdown-account-name.yml
@@ -0,0 +1,4 @@
+---
+title: Added profile name to user dropdown
+merge_request:
+author:
diff --git a/changelogs/unreleased/30493-env-deploy-tooltip.yml b/changelogs/unreleased/30493-env-deploy-tooltip.yml
deleted file mode 100644
index 8fadaaa7bd2..00000000000
--- a/changelogs/unreleased/30493-env-deploy-tooltip.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes HTML structure that was preventing the tooltip to disappear when hovering
- out of the button.
-merge_request:
-author:
diff --git a/changelogs/unreleased/30587-pipeline-icon-z.yml b/changelogs/unreleased/30587-pipeline-icon-z.yml
deleted file mode 100644
index 548d16ce142..00000000000
--- a/changelogs/unreleased/30587-pipeline-icon-z.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: fix Status icons overlapping sidebar on mobile
-merge_request:
-author:
diff --git a/changelogs/unreleased/30588-fix-javascript-sourcemaps-w-chrome-breakpoints.yml b/changelogs/unreleased/30588-fix-javascript-sourcemaps-w-chrome-breakpoints.yml
deleted file mode 100644
index 9cff3c2776f..00000000000
--- a/changelogs/unreleased/30588-fix-javascript-sourcemaps-w-chrome-breakpoints.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Upgrade webpack to v2.3.3 and webpack-dev-server to v2.4.2
-merge_request: 10552
-author:
diff --git a/changelogs/unreleased/30672-versioned-markdown-cache.yml b/changelogs/unreleased/30672-versioned-markdown-cache.yml
new file mode 100644
index 00000000000..d8f977b01de
--- /dev/null
+++ b/changelogs/unreleased/30672-versioned-markdown-cache.yml
@@ -0,0 +1,4 @@
+---
+title: Replace rake cache:clear:db with an automatic mechanism
+merge_request: 10597
+author:
diff --git a/changelogs/unreleased/30678-improve-dev-server-process.yml b/changelogs/unreleased/30678-improve-dev-server-process.yml
new file mode 100644
index 00000000000..efa2fc210e3
--- /dev/null
+++ b/changelogs/unreleased/30678-improve-dev-server-process.yml
@@ -0,0 +1,4 @@
+---
+title: Keep webpack-dev-server process functional across branch changes
+merge_request: 10581
+author:
diff --git a/changelogs/unreleased/31009-disable-test-settings-on-services-when-repository-is-empty.yml b/changelogs/unreleased/31009-disable-test-settings-on-services-when-repository-is-empty.yml
new file mode 100644
index 00000000000..6e43a032f20
--- /dev/null
+++ b/changelogs/unreleased/31009-disable-test-settings-on-services-when-repository-is-empty.yml
@@ -0,0 +1,4 @@
+---
+title: Disable test settings on chat notification services when repository is empty
+merge_request: 10759
+author:
diff --git a/changelogs/unreleased/31193-ff-copy.yml b/changelogs/unreleased/31193-ff-copy.yml
new file mode 100644
index 00000000000..4d44d83d458
--- /dev/null
+++ b/changelogs/unreleased/31193-ff-copy.yml
@@ -0,0 +1,4 @@
+---
+title: fix inline diff copy in firefox
+merge_request:
+author:
diff --git a/changelogs/unreleased/4195-add-sorting-to-project-milestones.yml b/changelogs/unreleased/4195-add-sorting-to-project-milestones.yml
deleted file mode 100644
index d4104dfa772..00000000000
--- a/changelogs/unreleased/4195-add-sorting-to-project-milestones.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add dropdown sort to project milestones
-merge_request:
-author: George Andrinopoulos
diff --git a/changelogs/unreleased/6260-frontend-prevent-authored-votes.yml b/changelogs/unreleased/6260-frontend-prevent-authored-votes.yml
new file mode 100644
index 00000000000..82e852fa197
--- /dev/null
+++ b/changelogs/unreleased/6260-frontend-prevent-authored-votes.yml
@@ -0,0 +1,4 @@
+---
+title: 'Frontend prevent authored votes'
+merge_request: 6260
+author: Barthc
diff --git a/changelogs/unreleased/8998_skip_pending_commits_if_not_head.yml b/changelogs/unreleased/8998_skip_pending_commits_if_not_head.yml
deleted file mode 100644
index 9852cd6e4ff..00000000000
--- a/changelogs/unreleased/8998_skip_pending_commits_if_not_head.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Cancel pending pipelines if commits not HEAD
-merge_request: 9362
-author: Rydkin Maxim
diff --git a/changelogs/unreleased/adam-finish-5993-closed-issuable.yml b/changelogs/unreleased/adam-finish-5993-closed-issuable.yml
deleted file mode 100644
index b324566313f..00000000000
--- a/changelogs/unreleased/adam-finish-5993-closed-issuable.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add indication for closed or merged issuables in GFM
-merge_request: 9462
-author: Adam Buckland
diff --git a/changelogs/unreleased/adam-prevent-two-issue-trackers.yml b/changelogs/unreleased/adam-prevent-two-issue-trackers.yml
deleted file mode 100644
index 307b7ec7359..00000000000
--- a/changelogs/unreleased/adam-prevent-two-issue-trackers.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent more than one issue tracker to be active for the same project
-merge_request:
-author: luisdgs19
diff --git a/changelogs/unreleased/add-blob-copy-button.yml b/changelogs/unreleased/add-blob-copy-button.yml
deleted file mode 100644
index 946723e523b..00000000000
--- a/changelogs/unreleased/add-blob-copy-button.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add copy button to blob header and use icon for Raw button
-merge_request:
-author:
diff --git a/changelogs/unreleased/add-dimension-etag-caching-metrics.yml b/changelogs/unreleased/add-dimension-etag-caching-metrics.yml
deleted file mode 100644
index f2a13eb7c61..00000000000
--- a/changelogs/unreleased/add-dimension-etag-caching-metrics.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Include endpoint in metrics for ETag caching middleware
-merge_request: 10495
-author:
diff --git a/changelogs/unreleased/add-error-empty-states.yml b/changelogs/unreleased/add-error-empty-states.yml
deleted file mode 100644
index ec6c7b6dce9..00000000000
--- a/changelogs/unreleased/add-error-empty-states.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Introduced error/empty states for the environments performance metrics
-merge_request: 10271
-author:
diff --git a/changelogs/unreleased/add-field-for-group-name.yml b/changelogs/unreleased/add-field-for-group-name.yml
deleted file mode 100644
index 0fe511a4fa1..00000000000
--- a/changelogs/unreleased/add-field-for-group-name.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add a name field to the group form
-merge_request: 9891
-author: Douglas Lovell
diff --git a/changelogs/unreleased/add-issue-modal-loading-indicator.yml b/changelogs/unreleased/add-issue-modal-loading-indicator.yml
deleted file mode 100644
index 5398399c018..00000000000
--- a/changelogs/unreleased/add-issue-modal-loading-indicator.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Shows loading icon in issue boards modal when changing filters
-merge_request:
-author:
diff --git a/changelogs/unreleased/add-labels-to-issue-hook.yml b/changelogs/unreleased/add-labels-to-issue-hook.yml
deleted file mode 100644
index 967430ee09f..00000000000
--- a/changelogs/unreleased/add-labels-to-issue-hook.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added labels array to the issue web hook returned object
-merge_request: 9972
-author:
diff --git a/changelogs/unreleased/add-test-backoff-util.yml b/changelogs/unreleased/add-test-backoff-util.yml
deleted file mode 100644
index f3f3b99caec..00000000000
--- a/changelogs/unreleased/add-test-backoff-util.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added tests for the w.gl.utils.backOff promise
-merge_request:
-author:
diff --git a/changelogs/unreleased/add-todos-shortcut.yml b/changelogs/unreleased/add-todos-shortcut.yml
deleted file mode 100644
index 41d42775937..00000000000
--- a/changelogs/unreleased/add-todos-shortcut.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add `g t` global shortcut to go to todos
-merge_request:
-author:
diff --git a/changelogs/unreleased/add-ui-for-trigger-schedule.yml b/changelogs/unreleased/add-ui-for-trigger-schedule.yml
deleted file mode 100644
index 9ca78983605..00000000000
--- a/changelogs/unreleased/add-ui-for-trigger-schedule.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add UI for Trigger Schedule
-merge_request: 10533
-author: dosuken123
diff --git a/changelogs/unreleased/add-vue-loader.yml b/changelogs/unreleased/add-vue-loader.yml
new file mode 100644
index 00000000000..382ef61ff21
--- /dev/null
+++ b/changelogs/unreleased/add-vue-loader.yml
@@ -0,0 +1,4 @@
+---
+title: add support for .vue templates
+merge_request: 10517
+author:
diff --git a/changelogs/unreleased/add_quick_submit_for_snippets_form.yml b/changelogs/unreleased/add_quick_submit_for_snippets_form.yml
deleted file mode 100644
index 088f1335796..00000000000
--- a/changelogs/unreleased/add_quick_submit_for_snippets_form.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add quick submit for snippet forms
-merge_request: 9911
-author: blackst0ne
diff --git a/changelogs/unreleased/add_remove_concurrent_index_to_database_helper.yml b/changelogs/unreleased/add_remove_concurrent_index_to_database_helper.yml
deleted file mode 100644
index c7b06e45607..00000000000
--- a/changelogs/unreleased/add_remove_concurrent_index_to_database_helper.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add remove_concurrent_index to database helper
-merge_request: 10441
-author: blackst0ne
diff --git a/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml b/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml
deleted file mode 100644
index c3c877423ff..00000000000
--- a/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix conflict resolution when files contain valid UTF-8 characters
-merge_request:
-author:
diff --git a/changelogs/unreleased/award-emoji-button-smiley-animation.yml b/changelogs/unreleased/award-emoji-button-smiley-animation.yml
deleted file mode 100644
index 31903aeb040..00000000000
--- a/changelogs/unreleased/award-emoji-button-smiley-animation.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added award emoji animation and improved active state
-merge_request:
-author:
diff --git a/changelogs/unreleased/bb_save_trace.yml b/changelogs/unreleased/bb_save_trace.yml
new file mode 100644
index 00000000000..6ff31f4f111
--- /dev/null
+++ b/changelogs/unreleased/bb_save_trace.yml
@@ -0,0 +1,5 @@
+---
+title: "[BB Importer] Save the error trace and the whole raw document to debug problems
+ easier"
+merge_request:
+author:
diff --git a/changelogs/unreleased/boards-done-add-tooltip.yml b/changelogs/unreleased/boards-done-add-tooltip.yml
new file mode 100644
index 00000000000..139f1efc8ee
--- /dev/null
+++ b/changelogs/unreleased/boards-done-add-tooltip.yml
@@ -0,0 +1,4 @@
+---
+title: Add tooltip to header of Done board
+merge_request: 10574
+author: Andy Brown
diff --git a/changelogs/unreleased/bug-api_milestone_merge_requests_scope.yml b/changelogs/unreleased/bug-api_milestone_merge_requests_scope.yml
deleted file mode 100644
index a1e1c29165e..00000000000
--- a/changelogs/unreleased/bug-api_milestone_merge_requests_scope.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes milestone/merge_requests endpoint to actually scope the result
-merge_request:
-author: Joren De Groof
diff --git a/changelogs/unreleased/bugfix-systemhook.yml b/changelogs/unreleased/bugfix-systemhook.yml
deleted file mode 100644
index 4c4d0dcc7a2..00000000000
--- a/changelogs/unreleased/bugfix-systemhook.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix bug when system hook for deploy key
-merge_request: 9796
-author: billy.lb
diff --git a/changelogs/unreleased/button-capitalization.yml b/changelogs/unreleased/button-capitalization.yml
deleted file mode 100644
index 13b3beea40c..00000000000
--- a/changelogs/unreleased/button-capitalization.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Changed capitalisation of buttons across GitLab
-merge_request: 10418
-author:
diff --git a/changelogs/unreleased/calendar-tooltips.yml b/changelogs/unreleased/calendar-tooltips.yml
deleted file mode 100644
index d1517bbab58..00000000000
--- a/changelogs/unreleased/calendar-tooltips.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add tooltip to user's calendar activities
-merge_request: 10123
-author: Alex Argunov
diff --git a/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml b/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml
deleted file mode 100644
index dc315ca2367..00000000000
--- a/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added remaining_time method to milestoneish, specs and updated the milestone_helper
- milestone_remaining_days method to correctly return the correct remaining time.
-merge_request:
-author: Michael Robinson
diff --git a/changelogs/unreleased/clean_carrierwave_tempfiles.yml b/changelogs/unreleased/clean_carrierwave_tempfiles.yml
deleted file mode 100644
index 53fa69700ff..00000000000
--- a/changelogs/unreleased/clean_carrierwave_tempfiles.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Periodically clean up temporary upload files to recover storage space
-merge_request: 9466
-author: blackst0ne
diff --git a/changelogs/unreleased/cleaner-additional-award-emoji-button.yml b/changelogs/unreleased/cleaner-additional-award-emoji-button.yml
deleted file mode 100644
index 84685f4bd45..00000000000
--- a/changelogs/unreleased/cleaner-additional-award-emoji-button.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Removed unnecessary 'add' text in additional award emoji button
-merge_request:
-author:
diff --git a/changelogs/unreleased/create-collapsed-todo-button.yml b/changelogs/unreleased/create-collapsed-todo-button.yml
deleted file mode 100644
index 6da6c070bf7..00000000000
--- a/changelogs/unreleased/create-collapsed-todo-button.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: adds todo functionality to closed issuable sidebar and changes todo bell icon
- to check-square
-merge_request:
-author:
diff --git a/changelogs/unreleased/dm-copy-code-as-gfm.yml b/changelogs/unreleased/dm-copy-code-as-gfm.yml
deleted file mode 100644
index 15ae2da44a3..00000000000
--- a/changelogs/unreleased/dm-copy-code-as-gfm.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Copy code as GFM from diffs, blobs and GFM code blocks
-merge_request:
-author:
diff --git a/changelogs/unreleased/dm-copy-diff-file-title-as-gfm.yml b/changelogs/unreleased/dm-copy-diff-file-title-as-gfm.yml
deleted file mode 100644
index 506883bc17d..00000000000
--- a/changelogs/unreleased/dm-copy-diff-file-title-as-gfm.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: After copying a diff file or blob path, pasting it into a comment field will format it as Markdown.
-merge_request: 9876
-author:
diff --git a/changelogs/unreleased/dm-link-discussion-to-outdated-diff.yml b/changelogs/unreleased/dm-link-discussion-to-outdated-diff.yml
new file mode 100644
index 00000000000..d489bada7ea
--- /dev/null
+++ b/changelogs/unreleased/dm-link-discussion-to-outdated-diff.yml
@@ -0,0 +1,4 @@
+---
+title: Link to outdated diff in older MR version from outdated diff discussion
+merge_request:
+author:
diff --git a/changelogs/unreleased/dz-cleanup-add-users.yml b/changelogs/unreleased/dz-cleanup-add-users.yml
new file mode 100644
index 00000000000..ba1e2d609f9
--- /dev/null
+++ b/changelogs/unreleased/dz-cleanup-add-users.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor add_users method for project and group
+merge_request: 10850
+author:
diff --git a/changelogs/unreleased/dz-fix-group-move.yml b/changelogs/unreleased/dz-fix-group-move.yml
deleted file mode 100644
index 51fbe04fdc2..00000000000
--- a/changelogs/unreleased/dz-fix-group-move.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix subgroup repository disappearance if group was moved
-merge_request: 10414
-author:
diff --git a/changelogs/unreleased/dz-hide-zero-counter.yml b/changelogs/unreleased/dz-hide-zero-counter.yml
deleted file mode 100644
index 45f35044c48..00000000000
--- a/changelogs/unreleased/dz-hide-zero-counter.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Hide header counters for issue/mr/todos if zero
-merge_request: 10506
-author:
diff --git a/changelogs/unreleased/dz-refactor-admin-group-members.yml b/changelogs/unreleased/dz-refactor-admin-group-members.yml
new file mode 100644
index 00000000000..993a6cac0df
--- /dev/null
+++ b/changelogs/unreleased/dz-refactor-admin-group-members.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor Admin::GroupsController#members_update method and add some specs
+merge_request: 10735
+author:
diff --git a/changelogs/unreleased/dz-refactor-create-members.yml b/changelogs/unreleased/dz-refactor-create-members.yml
new file mode 100644
index 00000000000..8cff21eabb1
--- /dev/null
+++ b/changelogs/unreleased/dz-refactor-create-members.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor code that creates project/group members
+merge_request: 10735
+author:
diff --git a/changelogs/unreleased/dz-remove-repo-version.yml b/changelogs/unreleased/dz-remove-repo-version.yml
new file mode 100644
index 00000000000..f9e51a920f9
--- /dev/null
+++ b/changelogs/unreleased/dz-remove-repo-version.yml
@@ -0,0 +1,4 @@
+---
+title: Remove Repository#version method and tests
+merge_request: 10734
+author:
diff --git a/changelogs/unreleased/emoji-menu-duplicated-search-icon.yml b/changelogs/unreleased/emoji-menu-duplicated-search-icon.yml
deleted file mode 100644
index 4ab6ba5399c..00000000000
--- a/changelogs/unreleased/emoji-menu-duplicated-search-icon.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Removed the duplicated search icon in the award emoji menu
-merge_request:
-author:
diff --git a/changelogs/unreleased/empty-task-list-alignment.yml b/changelogs/unreleased/empty-task-list-alignment.yml
new file mode 100644
index 00000000000..ca04e1cab5a
--- /dev/null
+++ b/changelogs/unreleased/empty-task-list-alignment.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed alignment of empty task list items
+merge_request:
+author:
diff --git a/changelogs/unreleased/enable-snippets-by-default.yml b/changelogs/unreleased/enable-snippets-by-default.yml
deleted file mode 100644
index 04fa3f7bdae..00000000000
--- a/changelogs/unreleased/enable-snippets-by-default.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable snippets for new projects by default
-merge_request:
-author:
diff --git a/changelogs/unreleased/environment-performance-improvements.yml b/changelogs/unreleased/environment-performance-improvements.yml
deleted file mode 100644
index 43e8f0afcee..00000000000
--- a/changelogs/unreleased/environment-performance-improvements.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improved UX for the environments metrics view
-merge_request: 9946
-author:
diff --git a/changelogs/unreleased/es6-class-issue.yml b/changelogs/unreleased/es6-class-issue.yml
deleted file mode 100644
index 9d1c3ac7421..00000000000
--- a/changelogs/unreleased/es6-class-issue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Convert Issue into ES6 class
-merge_request: 9636
-author: winniehell
diff --git a/changelogs/unreleased/feature-custom-lfs.yml b/changelogs/unreleased/feature-custom-lfs.yml
deleted file mode 100644
index ec968386a6f..00000000000
--- a/changelogs/unreleased/feature-custom-lfs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Do not show LFS object when LFS is disabled
-merge_request: 9779
-author: Christopher Bartz
diff --git a/changelogs/unreleased/feature-enforce-2fa-per-group.yml b/changelogs/unreleased/feature-enforce-2fa-per-group.yml
deleted file mode 100644
index 6dd99e4245f..00000000000
--- a/changelogs/unreleased/feature-enforce-2fa-per-group.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support 2FA requirement per-group
-merge_request: 8763
-author: Markus Koller
diff --git a/changelogs/unreleased/feature-gh-rake-task.yml b/changelogs/unreleased/feature-gh-rake-task.yml
deleted file mode 100644
index 5b1d380690c..00000000000
--- a/changelogs/unreleased/feature-gh-rake-task.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add rake task to import GitHub projects from the command line
-merge_request:
-author:
diff --git a/changelogs/unreleased/feature-multi-level-container-registry-images.yml b/changelogs/unreleased/feature-multi-level-container-registry-images.yml
deleted file mode 100644
index 6d39a6c17c0..00000000000
--- a/changelogs/unreleased/feature-multi-level-container-registry-images.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add support for multi-level container image repository names
-merge_request: 10109
-author: André Guede
diff --git a/changelogs/unreleased/feature-tokens-rake-task.yml b/changelogs/unreleased/feature-tokens-rake-task.yml
deleted file mode 100644
index 6c3845757db..00000000000
--- a/changelogs/unreleased/feature-tokens-rake-task.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: New rake task to reset all email and private tokens
-merge_request:
-author:
diff --git a/changelogs/unreleased/feature-use-gitaly-for-commit-is-ancestor.yml b/changelogs/unreleased/feature-use-gitaly-for-commit-is-ancestor.yml
deleted file mode 100644
index 733e3643ce5..00000000000
--- a/changelogs/unreleased/feature-use-gitaly-for-commit-is-ancestor.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use Gitaly for Repository#is_ancestor
-merge_request: 9864
-author:
diff --git a/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
deleted file mode 100644
index 4b668d994a1..00000000000
--- a/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use Gitaly for CommitController#show
-merge_request: 9629
-author:
diff --git a/changelogs/unreleased/file-import-export-path-disclosure.yml b/changelogs/unreleased/file-import-export-path-disclosure.yml
deleted file mode 100644
index 1a297d07187..00000000000
--- a/changelogs/unreleased/file-import-export-path-disclosure.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix path disclosure in project import/export
-merge_request:
-author:
-
diff --git a/changelogs/unreleased/fix-29093.yml b/changelogs/unreleased/fix-29093.yml
deleted file mode 100644
index 791129afe93..00000000000
--- a/changelogs/unreleased/fix-29093.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix 'Object not found - no match for id (sha)' when importing GitHub Pull Requests
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-admin-projects.yml b/changelogs/unreleased/fix-admin-projects.yml
deleted file mode 100644
index d192f07004c..00000000000
--- a/changelogs/unreleased/fix-admin-projects.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix layout of projects page on admin area
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml
deleted file mode 100644
index 4db684c40b2..00000000000
--- a/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Resolve project pipeline status caching problem on dashboard
-merge_request: 9895
-author:
diff --git a/changelogs/unreleased/fix-gb-remove-deprecated-pipeline-processing-code.yml b/changelogs/unreleased/fix-gb-remove-deprecated-pipeline-processing-code.yml
deleted file mode 100644
index 32862b527fd..00000000000
--- a/changelogs/unreleased/fix-gb-remove-deprecated-pipeline-processing-code.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Drop support for correctly processing legacy pipelines
-merge_request: 10266
-author:
diff --git a/changelogs/unreleased/fix-gh-import-status-check.yml b/changelogs/unreleased/fix-gh-import-status-check.yml
deleted file mode 100644
index d04bc2954a0..00000000000
--- a/changelogs/unreleased/fix-gh-import-status-check.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Periodically mark projects that are stuck in importing as failed
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-github-importer-slowness.yml b/changelogs/unreleased/fix-github-importer-slowness.yml
deleted file mode 100644
index c1f8d0e02d5..00000000000
--- a/changelogs/unreleased/fix-github-importer-slowness.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve performance of GitHub importer for large repositories.
-merge_request: 10273
-author:
diff --git a/changelogs/unreleased/fix-groups-long-url.yml b/changelogs/unreleased/fix-groups-long-url.yml
deleted file mode 100644
index f0f1296ad40..00000000000
--- a/changelogs/unreleased/fix-groups-long-url.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Skip groups validation on the client
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-import-fork.yml b/changelogs/unreleased/fix-import-fork.yml
deleted file mode 100644
index ff8dd131995..00000000000
--- a/changelogs/unreleased/fix-import-fork.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Import/Export MR diffs not showing and missing forked MRs
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-import-namespace.yml b/changelogs/unreleased/fix-import-namespace.yml
deleted file mode 100644
index 9a2fa5e425f..00000000000
--- a/changelogs/unreleased/fix-import-namespace.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Create subgroups if they don't exist while importing projects
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-issue-23237.yml b/changelogs/unreleased/fix-issue-23237.yml
deleted file mode 100644
index ed0ffc0684d..00000000000
--- a/changelogs/unreleased/fix-issue-23237.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Fixes an issue in the new merge request form, where a tag would be selected instead of a branch when they have the same names"
-merge_request: 9535
-author: Weiqing Chu
diff --git a/changelogs/unreleased/fix-link-prometheus-opening-outside-gitlab.yml b/changelogs/unreleased/fix-link-prometheus-opening-outside-gitlab.yml
new file mode 100644
index 00000000000..e684a1f6684
--- /dev/null
+++ b/changelogs/unreleased/fix-link-prometheus-opening-outside-gitlab.yml
@@ -0,0 +1,4 @@
+---
+title: Removed target blank from the metrics action inside the environments list
+merge_request: 10726
+author:
diff --git a/changelogs/unreleased/fix-milestone-name-on-show.yml b/changelogs/unreleased/fix-milestone-name-on-show.yml
deleted file mode 100644
index bf17a758c80..00000000000
--- a/changelogs/unreleased/fix-milestone-name-on-show.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Milestone name on show page
-merge_request:
-author: Raveesh
diff --git a/changelogs/unreleased/fix-user-profile-tabs-showing-raw-json-instead.yml b/changelogs/unreleased/fix-user-profile-tabs-showing-raw-json-instead.yml
new file mode 100644
index 00000000000..410172864e3
--- /dev/null
+++ b/changelogs/unreleased/fix-user-profile-tabs-showing-raw-json-instead.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent user profile tabs to display raw json when going back and forward in
+ browser history
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix_admin_monitoring_background.yml b/changelogs/unreleased/fix_admin_monitoring_background.yml
deleted file mode 100644
index 3a9a1c88672..00000000000
--- a/changelogs/unreleased/fix_admin_monitoring_background.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Handle parsing OpenBSD ps output properly to display sidekiq infos on admin->monitoring->background
-merge_request: 10303
-author: Sebastian Reitenbach
diff --git a/changelogs/unreleased/fix_cache_expiration_in_repository.yml b/changelogs/unreleased/fix_cache_expiration_in_repository.yml
new file mode 100644
index 00000000000..5f34f2bd040
--- /dev/null
+++ b/changelogs/unreleased/fix_cache_expiration_in_repository.yml
@@ -0,0 +1,4 @@
+---
+title: Fix redundant cache expiration in Repository
+merge_request: 10575
+author: blackst0ne
diff --git a/changelogs/unreleased/fix_link_in_readme.yml b/changelogs/unreleased/fix_link_in_readme.yml
new file mode 100644
index 00000000000..be5ceac8656
--- /dev/null
+++ b/changelogs/unreleased/fix_link_in_readme.yml
@@ -0,0 +1,4 @@
+---
+title: Fix dead link to GDK on the README page
+merge_request:
+author: Dino Maric
diff --git a/changelogs/unreleased/fix_rake_gitlab_check_sidekiq.yml b/changelogs/unreleased/fix_rake_gitlab_check_sidekiq.yml
deleted file mode 100644
index 4752ed34ae6..00000000000
--- a/changelogs/unreleased/fix_rake_gitlab_check_sidekiq.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Force unlimited terminal size when checking processes via call to ps
-merge_request: 10246
-author: Sebastian Reitenbach
diff --git a/changelogs/unreleased/fix_spaces_in_label_title.yml b/changelogs/unreleased/fix_spaces_in_label_title.yml
new file mode 100644
index 00000000000..51f07438edb
--- /dev/null
+++ b/changelogs/unreleased/fix_spaces_in_label_title.yml
@@ -0,0 +1,4 @@
+---
+title: Remove heading and trailing spaces from label's color and title
+merge_request: 10603
+author: blackst0ne
diff --git a/changelogs/unreleased/fix_updated_field_in_issues-atom.yml b/changelogs/unreleased/fix_updated_field_in_issues-atom.yml
deleted file mode 100644
index 414facdf779..00000000000
--- a/changelogs/unreleased/fix_updated_field_in_issues-atom.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix xml.updated field in rss/atom feeds
-merge_request: 9889
-author: blackst0ne
diff --git a/changelogs/unreleased/fix_visibility_level.yml b/changelogs/unreleased/fix_visibility_level.yml
deleted file mode 100644
index 4cf649124ca..00000000000
--- a/changelogs/unreleased/fix_visibility_level.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix visibility level on new project page
-merge_request: 9885
-author: blackst0ne
diff --git a/changelogs/unreleased/fix_wiki_commit_message.yml b/changelogs/unreleased/fix_wiki_commit_message.yml
deleted file mode 100644
index e5cd398b4b5..00000000000
--- a/changelogs/unreleased/fix_wiki_commit_message.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix wiki commit message
-merge_request: 10464
-author: blackst0ne
diff --git a/changelogs/unreleased/fl-remove-ujs-pipelines.yml b/changelogs/unreleased/fl-remove-ujs-pipelines.yml
deleted file mode 100644
index f353400753a..00000000000
--- a/changelogs/unreleased/fl-remove-ujs-pipelines.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Removes UJS from pipelines tables'
-merge_request: 9929
-author:
diff --git a/changelogs/unreleased/form-focus-previous-incorrect-form.yml b/changelogs/unreleased/form-focus-previous-incorrect-form.yml
new file mode 100644
index 00000000000..efabb78de6b
--- /dev/null
+++ b/changelogs/unreleased/form-focus-previous-incorrect-form.yml
@@ -0,0 +1,4 @@
+---
+title: Fixued preview shortcut focusing wrong preview tab
+merge_request:
+author:
diff --git a/changelogs/unreleased/gitaly-refs.yml b/changelogs/unreleased/gitaly-refs.yml
deleted file mode 100644
index 3d462cdf90f..00000000000
--- a/changelogs/unreleased/gitaly-refs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Incorporate Gitaly client for refs service
-merge_request: 9291
-author:
diff --git a/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml b/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml
deleted file mode 100644
index aff1bdd957c..00000000000
--- a/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Moved the gear settings dropdown to a tab in the groups view
-merge_request:
-author:
diff --git a/changelogs/unreleased/handle-failure-when-deleting-tags.yml b/changelogs/unreleased/handle-failure-when-deleting-tags.yml
deleted file mode 100644
index 99b07c5fb5f..00000000000
--- a/changelogs/unreleased/handle-failure-when-deleting-tags.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display error message when deleting tag in web UI fails
-merge_request: 9906
-author:
diff --git a/changelogs/unreleased/introduce-polling-interval-multiplier.yml b/changelogs/unreleased/introduce-polling-interval-multiplier.yml
deleted file mode 100644
index 3ccae8e327f..00000000000
--- a/changelogs/unreleased/introduce-polling-interval-multiplier.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Introduce "polling_interval_multiplier" as application setting
-merge_request: 10280
-author:
diff --git a/changelogs/unreleased/issue-boards-cant-drag-fix.yml b/changelogs/unreleased/issue-boards-cant-drag-fix.yml
deleted file mode 100644
index ac92573abe8..00000000000
--- a/changelogs/unreleased/issue-boards-cant-drag-fix.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed bug in issue boards which stopped cards being able to be dragged
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue-boards-new-search-bar.yml b/changelogs/unreleased/issue-boards-new-search-bar.yml
deleted file mode 100644
index b02be70c470..00000000000
--- a/changelogs/unreleased/issue-boards-new-search-bar.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added new filtered search bar to issue boards
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_27212.yml b/changelogs/unreleased/issue_27212.yml
deleted file mode 100644
index 7a7e04f7ca7..00000000000
--- a/changelogs/unreleased/issue_27212.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add closed_at field to issues
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_29449.yml b/changelogs/unreleased/issue_29449.yml
deleted file mode 100644
index 3556f22b080..00000000000
--- a/changelogs/unreleased/issue_29449.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove whitespace in group links
-merge_request: 9947
-author: Xurxo Méndez Pérez
diff --git a/changelogs/unreleased/issue_91_ee_backport.yml b/changelogs/unreleased/issue_91_ee_backport.yml
deleted file mode 100644
index 17bc0e435f3..00000000000
--- a/changelogs/unreleased/issue_91_ee_backport.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Do not set closed_at to nil when issue is reopened
-merge_request:
-author:
diff --git a/changelogs/unreleased/jej-group-name-disclosure.yml b/changelogs/unreleased/jej-group-name-disclosure.yml
deleted file mode 100644
index 9b8ab7082ef..00000000000
--- a/changelogs/unreleased/jej-group-name-disclosure.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed private group name disclosure via new/update forms
-merge_request:
-author:
diff --git a/changelogs/unreleased/make-karma-fast-again.yml b/changelogs/unreleased/make-karma-fast-again.yml
deleted file mode 100644
index 9b95e06954a..00000000000
--- a/changelogs/unreleased/make-karma-fast-again.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Only add code coverage instrumentation when generating coverage report
-merge_request: 9987
-author:
diff --git a/changelogs/unreleased/make_user_mentions_case_insensitive.yml b/changelogs/unreleased/make_user_mentions_case_insensitive.yml
deleted file mode 100644
index ab114494802..00000000000
--- a/changelogs/unreleased/make_user_mentions_case_insensitive.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make user mentions case-insensitive
-merge_request: 10285
-author: blackst0ne
diff --git a/changelogs/unreleased/menu-shortcut.yml b/changelogs/unreleased/menu-shortcut.yml
deleted file mode 100644
index 74803498f58..00000000000
--- a/changelogs/unreleased/menu-shortcut.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add keyboard shortcuts to main menu
-merge_request:
-author:
diff --git a/changelogs/unreleased/metrics-button-misplaced.yml b/changelogs/unreleased/metrics-button-misplaced.yml
deleted file mode 100644
index 6c685ff32a5..00000000000
--- a/changelogs/unreleased/metrics-button-misplaced.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Moved the monitoring button inside the show view for the environments page
-merge_request:
-author:
diff --git a/changelogs/unreleased/microsoft-teams-integration.yml b/changelogs/unreleased/microsoft-teams-integration.yml
deleted file mode 100644
index c01902d3401..00000000000
--- a/changelogs/unreleased/microsoft-teams-integration.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Integrates Microsoft Teams webhooks with GitLab
-merge_request: 10412
-author:
diff --git a/changelogs/unreleased/milestone-not-showing-correctly-title.yml b/changelogs/unreleased/milestone-not-showing-correctly-title.yml
new file mode 100644
index 00000000000..7c21094d737
--- /dev/null
+++ b/changelogs/unreleased/milestone-not-showing-correctly-title.yml
@@ -0,0 +1,4 @@
+---
+title: Removed the milestone references from the milestone views
+merge_request:
+author:
diff --git a/changelogs/unreleased/move-search-labels.yml b/changelogs/unreleased/move-search-labels.yml
new file mode 100644
index 00000000000..3a1d23d622e
--- /dev/null
+++ b/changelogs/unreleased/move-search-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Move labels of search results from bottom to title
+merge_request: 10705
+author: dr
diff --git a/changelogs/unreleased/mr-diffs-speed-up.yml b/changelogs/unreleased/mr-diffs-speed-up.yml
deleted file mode 100644
index ccc7a99d05e..00000000000
--- a/changelogs/unreleased/mr-diffs-speed-up.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Speed up initial rendering of MR diffs page
-merge_request:
-author:
diff --git a/changelogs/unreleased/mr-new-page-changing-url.yml b/changelogs/unreleased/mr-new-page-changing-url.yml
deleted file mode 100644
index 39de1eaa523..00000000000
--- a/changelogs/unreleased/mr-new-page-changing-url.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed tabs on new merge request page causing incorrect URLs
-merge_request:
-author:
diff --git a/changelogs/unreleased/namespace-race-condition.yml b/changelogs/unreleased/namespace-race-condition.yml
deleted file mode 100644
index 2a76b6c74e8..00000000000
--- a/changelogs/unreleased/namespace-race-condition.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix project creation failure due to race condition in namespace directory creation
-merge_request: 10268
-author: Robin Bobbitt
diff --git a/changelogs/unreleased/new-resolvable-discussion.yml b/changelogs/unreleased/new-resolvable-discussion.yml
deleted file mode 100644
index f4dc4ea3ede..00000000000
--- a/changelogs/unreleased/new-resolvable-discussion.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add option to start a new resolvable discussion in an MR
-merge_request: 7527
-author:
diff --git a/changelogs/unreleased/omnibus-gitlab-1993-check-shell-repositories-path-group-is-root.yml b/changelogs/unreleased/omnibus-gitlab-1993-check-shell-repositories-path-group-is-root.yml
new file mode 100644
index 00000000000..3b9284258cb
--- /dev/null
+++ b/changelogs/unreleased/omnibus-gitlab-1993-check-shell-repositories-path-group-is-root.yml
@@ -0,0 +1,4 @@
+---
+title: "Make the `gitlab:gitlab_shell:check` task check that the repositories storage path are owned by the `root` group"
+merge_request:
+author:
diff --git a/changelogs/unreleased/open-redirect-continue-params.yml b/changelogs/unreleased/open-redirect-continue-params.yml
deleted file mode 100644
index def3bc7d929..00000000000
--- a/changelogs/unreleased/open-redirect-continue-params.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
-merge_request:
-author:
diff --git a/changelogs/unreleased/open-redirect-host-field.yml b/changelogs/unreleased/open-redirect-host-field.yml
deleted file mode 100644
index bed4b47cf04..00000000000
--- a/changelogs/unreleased/open-redirect-host-field.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
-merge_request:
-author:
diff --git a/changelogs/unreleased/optimise-builds-view.yml b/changelogs/unreleased/optimise-builds-view.yml
deleted file mode 100644
index 1d715ab4f47..00000000000
--- a/changelogs/unreleased/optimise-builds-view.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Optimise builds endpoint
-merge_request:
-author:
diff --git a/changelogs/unreleased/option-to-be-notified-of-own-activity.yml b/changelogs/unreleased/option-to-be-notified-of-own-activity.yml
deleted file mode 100644
index 542287a09be..00000000000
--- a/changelogs/unreleased/option-to-be-notified-of-own-activity.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add option to receive email notifications about your own activity
-merge_request: 10032
-author: Richard Macklin
diff --git a/changelogs/unreleased/pages-debug-log.yml b/changelogs/unreleased/pages-debug-log.yml
deleted file mode 100644
index 328c8e4615b..00000000000
--- a/changelogs/unreleased/pages-debug-log.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Log errors during generating of Gitlab Pages to debug log
-merge_request: 10335
-author: Danilo Bargen
diff --git a/changelogs/unreleased/pipeline-tooltips-overflow.yml b/changelogs/unreleased/pipeline-tooltips-overflow.yml
deleted file mode 100644
index 184da8049f3..00000000000
--- a/changelogs/unreleased/pipeline-tooltips-overflow.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed pipeline actions tooltips overflowing
-merge_request:
-author:
diff --git a/changelogs/unreleased/pipelines-build-tooltip.yml b/changelogs/unreleased/pipelines-build-tooltip.yml
deleted file mode 100644
index 000276e1de3..00000000000
--- a/changelogs/unreleased/pipelines-build-tooltip.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed job tooltip being cut-off
-merge_request:
-author:
diff --git a/changelogs/unreleased/projects-list-line-breaks.yml b/changelogs/unreleased/projects-list-line-breaks.yml
deleted file mode 100644
index 179d7081293..00000000000
--- a/changelogs/unreleased/projects-list-line-breaks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed projects list lines breaking
-merge_request:
-author:
diff --git a/changelogs/unreleased/query-users-by-extern-uid.yml b/changelogs/unreleased/query-users-by-extern-uid.yml
new file mode 100644
index 00000000000..39d1cf8d3f3
--- /dev/null
+++ b/changelogs/unreleased/query-users-by-extern-uid.yml
@@ -0,0 +1,4 @@
+---
+title: Implement search by extern_uid in Users API
+merge_request: 10509
+author: Robin Bobbitt
diff --git a/changelogs/unreleased/quiet-pipelines.yml b/changelogs/unreleased/quiet-pipelines.yml
deleted file mode 100644
index c02eb59b824..00000000000
--- a/changelogs/unreleased/quiet-pipelines.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only email pipeline creators; only email for successful pipelines with custom
- settings
-merge_request:
-author:
diff --git a/changelogs/unreleased/refresh-permissions-recent-users.yml b/changelogs/unreleased/refresh-permissions-recent-users.yml
deleted file mode 100644
index 4d08be6ed5c..00000000000
--- a/changelogs/unreleased/refresh-permissions-recent-users.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Reset users.authorized_projects_populated to automatically refresh user permissions
-merge_request:
-author:
diff --git a/changelogs/unreleased/remember-me-missasligned-mobile.yml b/changelogs/unreleased/remember-me-missasligned-mobile.yml
deleted file mode 100644
index 7071d32727f..00000000000
--- a/changelogs/unreleased/remember-me-missasligned-mobile.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Corrected alignment for the remember-me checkbox in the login view
-merge_request:
-author:
diff --git a/changelogs/unreleased/remove-double-newline-for-single-attachments.yml b/changelogs/unreleased/remove-double-newline-for-single-attachments.yml
new file mode 100644
index 00000000000..98a28e1ede1
--- /dev/null
+++ b/changelogs/unreleased/remove-double-newline-for-single-attachments.yml
@@ -0,0 +1,4 @@
+---
+title: Only add newlines between multiple uploads
+merge_request: 10545
+author:
diff --git a/changelogs/unreleased/remove_index_for_users-current_sign_in_at.yml b/changelogs/unreleased/remove_index_for_users-current_sign_in_at.yml
deleted file mode 100644
index ec3a2c8e2bf..00000000000
--- a/changelogs/unreleased/remove_index_for_users-current_sign_in_at.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove index for users.current sign in at
-merge_request: 10401
-author: blackst0ne
diff --git a/changelogs/unreleased/remove_is_admin.yml b/changelogs/unreleased/remove_is_admin.yml
deleted file mode 100644
index f6baf1942de..00000000000
--- a/changelogs/unreleased/remove_is_admin.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove the User#is_admin? method
-merge_request: 10520
-author: blackst0ne
diff --git a/changelogs/unreleased/rename_all_issues.yml b/changelogs/unreleased/rename_all_issues.yml
deleted file mode 100644
index d3109bdb17e..00000000000
--- a/changelogs/unreleased/rename_all_issues.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Rename 'All issues' to 'Open issues' in Add issues modal
-merge_request: 10042
-author: blackst0ne
diff --git a/changelogs/unreleased/rename_done_to_closed.yml b/changelogs/unreleased/rename_done_to_closed.yml
deleted file mode 100644
index 6de112c4b0d..00000000000
--- a/changelogs/unreleased/rename_done_to_closed.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change Done column to Closed in issue boards
-merge_request: 10198
-author: blackst0ne
diff --git a/changelogs/unreleased/replace_closing_mr_icon.yml b/changelogs/unreleased/replace_closing_mr_icon.yml
deleted file mode 100644
index 4d7b5fa67a7..00000000000
--- a/changelogs/unreleased/replace_closing_mr_icon.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace closing MR icon
-merge_request: 10103
-author: blackst0ne
diff --git a/changelogs/unreleased/reset-new-branch-button.yml b/changelogs/unreleased/reset-new-branch-button.yml
new file mode 100644
index 00000000000..318ee46298f
--- /dev/null
+++ b/changelogs/unreleased/reset-new-branch-button.yml
@@ -0,0 +1,4 @@
+---
+title: Reset New branch button when issue state changes
+merge_request: 5962
+author: winniehell
diff --git a/changelogs/unreleased/right-sidebar-closed-default-mobile.yml b/changelogs/unreleased/right-sidebar-closed-default-mobile.yml
new file mode 100644
index 00000000000..cf0ec418f0e
--- /dev/null
+++ b/changelogs/unreleased/right-sidebar-closed-default-mobile.yml
@@ -0,0 +1,4 @@
+---
+title: Set the issuable sidebar to remain closed for mobile devices
+merge_request:
+author:
diff --git a/changelogs/unreleased/scrollable-secondary-tabs.yml b/changelogs/unreleased/scrollable-secondary-tabs.yml
deleted file mode 100644
index 963d5d325dc..00000000000
--- a/changelogs/unreleased/scrollable-secondary-tabs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed tabs not scrolling on mobile
-merge_request:
-author:
diff --git a/changelogs/unreleased/sh-fix-ssh-keys-with-spaces.yml b/changelogs/unreleased/sh-fix-ssh-keys-with-spaces.yml
deleted file mode 100644
index fe75d7e1156..00000000000
--- a/changelogs/unreleased/sh-fix-ssh-keys-with-spaces.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Handle SSH keys that have multiple spaces between each marker
-merge_request:
-author:
diff --git a/changelogs/unreleased/sh-relax-wiki-slug-constraint.yml b/changelogs/unreleased/sh-relax-wiki-slug-constraint.yml
deleted file mode 100644
index 08395b0d28c..00000000000
--- a/changelogs/unreleased/sh-relax-wiki-slug-constraint.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Relax constraint on Wiki IDs, since subdirectories can contain spaces
-merge_request:
-author:
diff --git a/changelogs/unreleased/sh-remove-tags-from-explore.yml b/changelogs/unreleased/sh-remove-tags-from-explore.yml
deleted file mode 100644
index b76ec89a006..00000000000
--- a/changelogs/unreleased/sh-remove-tags-from-explore.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove Tags filter from Projects Explore dropdown
-merge_request:
-author:
diff --git a/changelogs/unreleased/simplify-docs-trigger.yml b/changelogs/unreleased/simplify-docs-trigger.yml
deleted file mode 100644
index 062626359ef..00000000000
--- a/changelogs/unreleased/simplify-docs-trigger.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Simplify trigger_docs build job for CE and EE
-merge_request: 9820
-author: winniehell
diff --git a/changelogs/unreleased/spec_for_schema.yml b/changelogs/unreleased/spec_for_schema.yml
new file mode 100644
index 00000000000..7ea0b8672ce
--- /dev/null
+++ b/changelogs/unreleased/spec_for_schema.yml
@@ -0,0 +1,4 @@
+---
+title: Add spec for schema.rb
+merge_request: 10580
+author: blackst0ne
diff --git a/changelogs/unreleased/style-proc-cop.yml b/changelogs/unreleased/style-proc-cop.yml
deleted file mode 100644
index 25acab740bd..00000000000
--- a/changelogs/unreleased/style-proc-cop.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable Style/Proc cop for rubocop
-merge_request:
-author: mhasbini
diff --git a/changelogs/unreleased/submodules-no-dotgit.yml b/changelogs/unreleased/submodules-no-dotgit.yml
new file mode 100644
index 00000000000..2ff0ee997fa
--- /dev/null
+++ b/changelogs/unreleased/submodules-no-dotgit.yml
@@ -0,0 +1,4 @@
+---
+title: 'repository browser: handle submodule urls that don''t end with .git'
+merge_request:
+author: David Turner
diff --git a/changelogs/unreleased/tc-fix-pipeline-recipient.yml b/changelogs/unreleased/tc-fix-pipeline-recipient.yml
deleted file mode 100644
index 0337533fdb2..00000000000
--- a/changelogs/unreleased/tc-fix-pipeline-recipient.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Clearly show who triggered the pipeline in email
-merge_request: 10283
-author:
diff --git a/changelogs/unreleased/tc-fix-unplayable-build-action-404.yml b/changelogs/unreleased/tc-fix-unplayable-build-action-404.yml
deleted file mode 100644
index e5e22c1daf7..00000000000
--- a/changelogs/unreleased/tc-fix-unplayable-build-action-404.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Disable pipeline and environment actions that are not playable
-merge_request: 10052
-author:
diff --git a/changelogs/unreleased/tc-pipeline-show-trigger-date.yml b/changelogs/unreleased/tc-pipeline-show-trigger-date.yml
deleted file mode 100644
index 4de784d98f3..00000000000
--- a/changelogs/unreleased/tc-pipeline-show-trigger-date.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show correct user & creation time in heading of the pipeline page
-merge_request: 9936
-author:
diff --git a/changelogs/unreleased/tc-show-pipeline-coverage-if-avail.yml b/changelogs/unreleased/tc-show-pipeline-coverage-if-avail.yml
deleted file mode 100644
index c0cc4fb18c8..00000000000
--- a/changelogs/unreleased/tc-show-pipeline-coverage-if-avail.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show the build/pipeline coverage if it is available
-merge_request:
-author:
diff --git a/changelogs/unreleased/time-tracking-color-not-consistent.yml b/changelogs/unreleased/time-tracking-color-not-consistent.yml
deleted file mode 100644
index 50ec9efb1ff..00000000000
--- a/changelogs/unreleased/time-tracking-color-not-consistent.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Corrected time tracking icon color in the issuable side bar
-merge_request:
-author:
diff --git a/changelogs/unreleased/uassign_on_member_removing.yml b/changelogs/unreleased/uassign_on_member_removing.yml
new file mode 100644
index 00000000000..cd60bdf5b3d
--- /dev/null
+++ b/changelogs/unreleased/uassign_on_member_removing.yml
@@ -0,0 +1,4 @@
+---
+title: Unassign all Issues and Merge Requests when member leaves a team
+merge_request:
+author:
diff --git a/changelogs/unreleased/update-issue-board-cards-design.yml b/changelogs/unreleased/update-issue-board-cards-design.yml
deleted file mode 100644
index 5ef94a74e8a..00000000000
--- a/changelogs/unreleased/update-issue-board-cards-design.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update issue board cards design
-merge_request: 10353
-author:
diff --git a/changelogs/unreleased/update-test-bundle-ignored-files.yml b/changelogs/unreleased/update-test-bundle-ignored-files.yml
deleted file mode 100644
index 1235d4ced6c..00000000000
--- a/changelogs/unreleased/update-test-bundle-ignored-files.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: update test_bundle.js ignored files
-merge_request:
-author:
diff --git a/changelogs/unreleased/use-corejs-polyfills.yml b/changelogs/unreleased/use-corejs-polyfills.yml
deleted file mode 100644
index 381f80c5c0d..00000000000
--- a/changelogs/unreleased/use-corejs-polyfills.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Standardize on core-js for es2015 polyfills
-merge_request: 9749
-author:
diff --git a/changelogs/unreleased/use-hashie-forbidden_attributes.yml b/changelogs/unreleased/use-hashie-forbidden_attributes.yml
new file mode 100644
index 00000000000..4f429b03a0d
--- /dev/null
+++ b/changelogs/unreleased/use-hashie-forbidden_attributes.yml
@@ -0,0 +1,4 @@
+---
+title: Add hashie-forbidden_attributes gem
+merge_request: 10579
+author: Andy Brown
diff --git a/changelogs/unreleased/user-activity-scroll-bar.yml b/changelogs/unreleased/user-activity-scroll-bar.yml
new file mode 100644
index 00000000000..97cccee42cb
--- /dev/null
+++ b/changelogs/unreleased/user-activity-scroll-bar.yml
@@ -0,0 +1,4 @@
+---
+title: Fix preemptive scroll bar on user activity calendar.
+merge_request: !10636
+author:
diff --git a/changelogs/unreleased/user-callout-showing-on-all-profiles.yml b/changelogs/unreleased/user-callout-showing-on-all-profiles.yml
deleted file mode 100644
index b8eb5a149b7..00000000000
--- a/changelogs/unreleased/user-callout-showing-on-all-profiles.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: User callout only shows on current users profile
-merge_request:
-author:
diff --git a/changelogs/unreleased/user-profile-join-date.yml b/changelogs/unreleased/user-profile-join-date.yml
deleted file mode 100644
index f9d78b0dc3e..00000000000
--- a/changelogs/unreleased/user-profile-join-date.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Removed the hours & minutes from the users start date on their profile
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-chat-notification-default-branch.yml b/changelogs/unreleased/zj-chat-notification-default-branch.yml
deleted file mode 100644
index fa0052d5034..00000000000
--- a/changelogs/unreleased/zj-chat-notification-default-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Only send chat notifications for the default branch
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-kube-service-auto-fill.yml b/changelogs/unreleased/zj-kube-service-auto-fill.yml
deleted file mode 100644
index 7a2c7a5085b..00000000000
--- a/changelogs/unreleased/zj-kube-service-auto-fill.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't fill in the default kubernetes namespace
-merge_request:
-author:
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index fdba1f6541e..59c7050a14d 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -344,3 +344,57 @@
:why: https://github.com/nodeca/pako/blob/master/LICENSE
:versions: []
:when: 2017-04-05 10:43:45.897720000 Z
+- - :approve
+ - caniuse-db
+ - :who: Mike Greiling
+ :why: https://github.com/Fyrd/caniuse/blob/master/LICENSE
+ :versions: []
+ :when: 2017-04-07 16:05:14.185549000 Z
+- - :approve
+ - domelementtype
+ - :who: Mike Greiling
+ :why: https://github.com/fb55/domelementtype/blob/master/LICENSE
+ :versions: []
+ :when: 2017-04-07 16:19:17.992640000 Z
+- - :approve
+ - domhandler
+ - :who: Mike Greiling
+ :why: https://github.com/fb55/domhandler/blob/master/LICENSE
+ :versions: []
+ :when: 2017-04-07 16:19:19.628953000 Z
+- - :approve
+ - domutils
+ - :who: Mike Greiling
+ :why: https://github.com/fb55/domutils/blob/master/LICENSE
+ :versions: []
+ :when: 2017-04-07 16:19:21.159356000 Z
+- - :approve
+ - entities
+ - :who: Mike Greiling
+ :why: https://github.com/fb55/entities/blob/master/LICENSE
+ :versions: []
+ :when: 2017-04-07 16:19:23.900571000 Z
+- - :approve
+ - ansi-html
+ - :who: Mike Greiling
+ :why: https://github.com/Tjatse/ansi-html/blob/master/LICENSE
+ :versions: []
+ :when: 2017-04-10 05:42:12.898178000 Z
+- - :approve
+ - map-stream
+ - :who: Mike Greiling
+ :why: https://github.com/dominictarr/map-stream/blob/master/LICENCE
+ :versions: []
+ :when: 2017-04-10 06:27:52.269085000 Z
+- - :approve
+ - pause-stream
+ - :who: Mike Greiling
+ :why: https://github.com/dominictarr/pause-stream/blob/master/LICENSE
+ :versions: []
+ :when: 2017-04-10 06:28:39.825894000 Z
+- - :approve
+ - undefsafe
+ - :who: Mike Greiling
+ :why: https://github.com/remy/undefsafe/blob/master/LICENSE
+ :versions: []
+ :when: 2017-04-10 06:30:00.002555000 Z
diff --git a/config/environments/test.rb b/config/environments/test.rb
index a25c5016a3b..c3b788c038e 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -8,7 +8,12 @@ Rails.application.configure do
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
- config.cache_classes = false
+
+ # Enabling caching of classes slows start-up time because all controllers
+ # are loaded at initalization, but it reduces memory and load because files
+ # are not reloaded with every request. For example, caching is not necessary
+ # for loading database migrations but useful for handling Knapsack specs.
+ config.cache_classes = ENV['CACHE_CLASSES'] == 'true'
# Configure static asset server for tests with Cache-Control for performance
config.assets.digest = false
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 3c70f35b9d0..06c9f734c2a 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -579,9 +579,9 @@ test:
storages:
default:
path: tmp/tests/repositories/
- gitaly_address: unix:<%= Rails.root.join('tmp/sockets/private/gitaly.socket') %>
+ gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
gitaly:
- enabled: false
+ enabled: true
backup:
path: tmp/tests/backups
gitlab_shell:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4c9d829aa9f..87bf48a3dcd 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -110,6 +110,14 @@ class Settings < Settingslogic
URI.parse(url_without_path).host
end
+
+ # Random cron time every Sunday to load balance usage pings
+ def cron_random_weekly_time
+ hour = rand(24)
+ minute = rand(60)
+
+ "#{minute} #{hour} * * 0"
+ end
end
end
@@ -204,8 +212,8 @@ Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings
Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab'
Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}"
Settings.gitlab['email_subject_suffix'] ||= ENV['GITLAB_EMAIL_SUBJECT_SUFFIX'] || ""
-Settings.gitlab['base_url'] ||= Settings.send(:build_base_gitlab_url)
-Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url)
+Settings.gitlab['base_url'] ||= Settings.__send__(:build_base_gitlab_url)
+Settings.gitlab['url'] ||= Settings.__send__(:build_gitlab_url)
Settings.gitlab['user'] ||= 'git'
Settings.gitlab['user_home'] ||= begin
Etc.getpwnam(Settings.gitlab['user']).dir
@@ -215,7 +223,7 @@ end
Settings.gitlab['time_zone'] ||= nil
Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil?
Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil?
-Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
+Settings.gitlab['restricted_visibility_levels'] = Settings.__send__(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil?
Settings.gitlab['default_projects_features'] ||= {}
@@ -228,7 +236,7 @@ Settings.gitlab.default_projects_features['wiki'] = true if Settin
Settings.gitlab.default_projects_features['snippets'] = true if Settings.gitlab.default_projects_features['snippets'].nil?
Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil?
Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil?
-Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
+Settings.gitlab.default_projects_features['visibility_level'] = Settings.__send__(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['domain_whitelist'] ||= []
Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project gitea]
Settings.gitlab['trusted_proxies'] ||= []
@@ -242,7 +250,7 @@ Settings.gitlab_ci['shared_runners_enabled'] = true if Settings.gitlab_ci['share
Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil?
Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil?
Settings.gitlab_ci['builds_path'] = Settings.absolute(Settings.gitlab_ci['builds_path'] || "builds/")
-Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url)
+Settings.gitlab_ci['url'] ||= Settings.__send__(:build_gitlab_ci_url)
#
# Reply by email
@@ -281,7 +289,7 @@ Settings.pages['https'] = false if Settings.pages['https'].nil?
Settings.pages['host'] ||= "example.com"
Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
-Settings.pages['url'] ||= Settings.send(:build_pages_url)
+Settings.pages['url'] ||= Settings.__send__(:build_pages_url)
Settings.pages['external_http'] ||= false unless Settings.pages['external_http'].present?
Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present?
@@ -355,6 +363,14 @@ Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'Rem
Settings.cron_jobs['stuck_import_jobs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_import_jobs_worker']['cron'] ||= '15 * * * *'
Settings.cron_jobs['stuck_import_jobs_worker']['job_class'] = 'StuckImportJobsWorker'
+Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.__send__(:cron_random_weekly_time)
+Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker'
+
+# Every day at 00:30
+Settings.cron_jobs['schedule_update_user_activity_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['schedule_update_user_activity_worker']['cron'] ||= '30 0 * * *'
+Settings.cron_jobs['schedule_update_user_activity_worker']['job_class'] = 'ScheduleUpdateUserActivityWorker'
#
# GitLab Shell
@@ -369,7 +385,7 @@ Settings.gitlab_shell['ssh_host'] ||= Settings.gitlab.ssh_host
Settings.gitlab_shell['ssh_port'] ||= 22
Settings.gitlab_shell['ssh_user'] ||= Settings.gitlab.user
Settings.gitlab_shell['owner_group'] ||= Settings.gitlab.user
-Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_ssh_path_prefix)
+Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.__send__(:build_gitlab_shell_ssh_path_prefix)
#
# Repositories
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 486ce3c5c87..52ba10604d4 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -91,6 +91,7 @@ namespace :admin do
resource :application_settings, only: [:show, :update] do
resources :services, only: [:index, :edit, :update]
+ get :usage_data
put :reset_runners_token
put :reset_health_check_token
put :clear_repository_check_states
@@ -105,6 +106,8 @@ namespace :admin do
end
end
+ resources :cohorts, only: :index
+
resources :builds, only: :index do
collection do
post :cancel_all
diff --git a/config/routes/project.rb b/config/routes/project.rb
index f5009186344..fa92202c1ea 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -42,29 +42,6 @@ constraints(ProjectUrlConstrainer.new) do
resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ }
end
- resources :compare, only: [:index, :create] do
- collection do
- get :diff_for_path
- end
- end
-
- get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
-
- # Don't use format parameter as file extension (old 3.0.x behavior)
- # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
- scope format: false do
- resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
-
- resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
- member do
- get :charts
- get :commits
- get :ci
- get :languages
- end
- end
- end
-
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
@@ -128,15 +105,6 @@ constraints(ProjectUrlConstrainer.new) do
end
end
- resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
- delete :merged_branches, controller: 'branches', action: :destroy_all_merged
- resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
- resource :release, only: [:edit, :update]
- end
-
- resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
- resources :protected_tags, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
-
resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :triggers, only: [:index, :create, :edit, :update, :destroy] do
member do
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index f8966c5ae75..5cf37a06e97 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -1,4 +1,4 @@
-# All routing related to repositoty browsing
+# All routing related to repository browsing
resource :repository, only: [:create] do
member do
@@ -6,83 +6,84 @@ resource :repository, only: [:create] do
end
end
-resources :refs, only: [] do
- collection do
- get 'switch'
+# Don't use format parameter as file extension (old 3.0.x behavior)
+# See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
+scope format: false do
+ get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
+
+ resources :compare, only: [:index, :create] do
+ collection do
+ get :diff_for_path
+ end
end
- member do
- # tree viewer logs
- get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
- # Directories with leading dots erroneously get rejected if git
- # ref regex used in constraints. Regex verification now done in controller.
- get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
- id: /.*/,
- path: /.*/
- }
+ resources :refs, only: [] do
+ collection do
+ get 'switch'
+ end
+
+ member do
+ # tree viewer logs
+ get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
+ # Directories with leading dots erroneously get rejected if git
+ # ref regex used in constraints. Regex verification now done in controller.
+ get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: {
+ id: /.*/,
+ path: /.*/
+ }
+ end
end
-end
-get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
-post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
-get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
-put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
-post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
+ scope constraints: { id: Gitlab::Regex.git_reference_regex } do
+ resources :network, only: [:show]
-scope('/blob/*id', as: :blob, controller: :blob, constraints: { id: /.+/, format: false }) do
- get :diff
- get '/', action: :show
- delete '/', action: :destroy
- post '/', action: :create
- put '/', action: :update
-end
+ resources :graphs, only: [:show] do
+ member do
+ get :charts
+ get :commits
+ get :ci
+ get :languages
+ end
+ end
-get(
- '/raw/*id',
- to: 'raw#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :raw
-)
+ resources :branches, only: [:index, :new, :create, :destroy]
+ delete :merged_branches, controller: 'branches', action: :destroy_all_merged
+ resources :tags, only: [:index, :show, :new, :create, :destroy] do
+ resource :release, only: [:edit, :update]
+ end
-get(
- '/tree/*id',
- to: 'tree#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :tree
-)
+ resources :protected_branches, only: [:index, :show, :create, :update, :destroy]
+ resources :protected_tags, only: [:index, :show, :create, :update, :destroy]
+ end
+
+ scope constraints: { id: /.+/ } do
+ scope controller: :blob do
+ get '/new/*id', action: :new, as: :new_blob
+ post '/create/*id', action: :create, as: :create_blob
+ get '/edit/*id', action: :edit, as: :edit_blob
+ put '/update/*id', action: :update, as: :update_blob
+ post '/preview/*id', action: :preview, as: :preview_blob
-get(
- '/find_file/*id',
- to: 'find_file#show',
- constraints: { id: /.+/, format: /html/ },
- as: :find_file
-)
+ scope path: '/blob/*id', as: :blob do
+ get :diff
+ get '/', action: :show
+ delete '/', action: :destroy
+ post '/', action: :create
+ put '/', action: :update
+ end
+ end
-get(
- '/files/*id',
- to: 'find_file#list',
- constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
- as: :files
-)
+ get '/tree/*id', to: 'tree#show', as: :tree
+ get '/raw/*id', to: 'raw#show', as: :raw
+ get '/blame/*id', to: 'blame#show', as: :blame
+ get '/commits/*id', to: 'commits#show', as: :commits
-post(
- '/create_dir/*id',
- to: 'tree#create_dir',
- constraints: { id: /.+/ },
- as: 'create_dir'
-)
+ post '/create_dir/*id', to: 'tree#create_dir', as: :create_dir
-get(
- '/blame/*id',
- to: 'blame#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :blame
-)
+ scope controller: :find_file do
+ get '/find_file/*id', action: :show, as: :find_file
-# File/dir history
-get(
- '/commits/*id',
- to: 'commits#show',
- constraints: { id: /.+/, format: false },
- as: :commits
-)
+ get '/files/*id', action: :list, as: :files
+ end
+ end
+end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 9d2066a6490..c3bd73533d0 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -34,7 +34,6 @@
- [repository_fork, 1]
- [repository_import, 1]
- [project_service, 1]
- - [clear_database_cache, 1]
- [delete_user, 1]
- [delete_merged_branches, 1]
- [authorized_projects, 1]
@@ -53,3 +52,4 @@
- [default, 1]
- [pages, 1]
- [system_hook_push, 1]
+ - [update_user_activity, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index e3bc939d578..cb0a57a3a41 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -6,10 +6,12 @@ var webpack = require('webpack');
var StatsPlugin = require('stats-webpack-plugin');
var CompressionPlugin = require('compression-webpack-plugin');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
+var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
var ROOT_PATH = path.resolve(__dirname, '..');
var IS_PRODUCTION = process.env.NODE_ENV === 'production';
var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1;
+var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
@@ -17,12 +19,11 @@ var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
var config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: {
+ blob: './blob_edit/blob_bundle.js',
+ boards: './boards/boards_bundle.js',
common: './commons/index.js',
common_vue: ['vue', './vue_shared/common_vue.js'],
common_d3: ['d3'],
- main: './main.js',
- blob: './blob_edit/blob_bundle.js',
- boards: './boards/boards_bundle.js',
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js',
@@ -30,26 +31,27 @@ var config = {
environments_folder: './environments/folder/environments_folder_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js',
graphs: './graphs/graphs_bundle.js',
+ group: './group.js',
groups_list: './groups_list.js',
issuable: './issuable/issuable_bundle.js',
+ issue_show: './issue_show/index.js',
+ main: './main.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
merge_request_widget: './merge_request_widget/ci_bundle.js',
monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js',
- sketch_viewer: './blob/sketch_viewer.js',
pdf_viewer: './blob/pdf_viewer.js',
+ pipelines: './pipelines/index.js',
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
protected_tags: './protected_tags',
snippet: './snippet/snippet_bundle.js',
+ sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js',
u2f: ['vendor/u2f'],
users: './users/users_bundle.js',
- vue_pipelines: './vue_pipelines_index/index.js',
- issue_show: './issue_show/index.js',
- group: './group.js',
},
output: {
@@ -65,13 +67,18 @@ var config = {
{
test: /\.js$/,
exclude: /(node_modules|vendor\/assets)/,
- loader: 'babel-loader'
+ loader: 'babel-loader',
+ },
+ {
+ test: /\.vue$/,
+ loader: 'vue-loader',
},
{
test: /\.svg$/,
- use: 'raw-loader'
- }, {
- test: /\.(worker.js|pdf)$/,
+ loader: 'raw-loader',
+ },
+ {
+ test: /\.(worker\.js|pdf)$/,
exclude: /node_modules/,
loader: 'file-loader',
},
@@ -114,10 +121,11 @@ var config = {
'environments',
'environments_folder',
'issuable',
+ 'issue_show',
'merge_conflicts',
'notebook_viewer',
'pdf_viewer',
- 'vue_pipelines',
+ 'pipelines',
],
minChunks: function(module, count) {
return module.resource && (/vue_shared/).test(module.resource);
@@ -176,12 +184,17 @@ if (IS_PRODUCTION) {
if (IS_DEV_SERVER) {
config.devtool = 'cheap-module-eval-source-map';
config.devServer = {
+ host: DEV_SERVER_HOST,
port: DEV_SERVER_PORT,
headers: { 'Access-Control-Allow-Origin': '*' },
stats: 'errors-only',
inline: DEV_SERVER_LIVERELOAD
};
- config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath;
+ config.output.publicPath = '//' + DEV_SERVER_HOST + ':' + DEV_SERVER_PORT + config.output.publicPath;
+ config.plugins.push(
+ // watch node_modules for changes if we encounter a missing module compile error
+ new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules'))
+ );
}
if (WEBPACK_REPORT) {
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 4bc735916c1..0d7eb1a7c93 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -223,7 +223,9 @@ class Gitlab::Seeder::CycleAnalytics
end
Gitlab::Seeder.quiet do
- if ENV['SEED_CYCLE_ANALYTICS']
+ flag = 'SEED_CYCLE_ANALYTICS'
+
+ if ENV[flag]
Project.all.each do |project|
seeder = Gitlab::Seeder::CycleAnalytics.new(project)
seeder.seed!
@@ -235,6 +237,6 @@ Gitlab::Seeder.quiet do
seeder = Gitlab::Seeder::CycleAnalytics.new(Project.order(:id).first, perf: true)
seeder.seed_metrics!
else
- puts "Not running the cycle analytics seed file. Use the `SEED_CYCLE_ANALYTICS` environment variable to enable it."
+ puts "Skipped. Use the `#{flag}` environment variable to enable."
end
end
diff --git a/db/fixtures/development/20_nested_groups.rb b/db/fixtures/development/20_nested_groups.rb
index d8dddc3fee9..2bc78e120a5 100644
--- a/db/fixtures/development/20_nested_groups.rb
+++ b/db/fixtures/development/20_nested_groups.rb
@@ -27,43 +27,49 @@ end
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
- project_urls = [
- 'https://android.googlesource.com/platform/hardware/broadcom/libbt.git',
- 'https://android.googlesource.com/platform/hardware/broadcom/wlan.git',
- 'https://android.googlesource.com/platform/hardware/bsp/bootloader/intel/edison-u-boot.git',
- 'https://android.googlesource.com/platform/hardware/bsp/broadcom.git',
- 'https://android.googlesource.com/platform/hardware/bsp/freescale.git',
- 'https://android.googlesource.com/platform/hardware/bsp/imagination.git',
- 'https://android.googlesource.com/platform/hardware/bsp/intel.git',
- 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.1.git',
- 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.4.git'
- ]
+ flag = 'SEED_NESTED_GROUPS'
- user = User.admins.first
+ if ENV[flag]
+ project_urls = [
+ 'https://android.googlesource.com/platform/hardware/broadcom/libbt.git',
+ 'https://android.googlesource.com/platform/hardware/broadcom/wlan.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/bootloader/intel/edison-u-boot.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/broadcom.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/freescale.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/imagination.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/intel.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.1.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.4.git'
+ ]
- project_urls.each_with_index do |url, i|
- full_path = url.sub('https://android.googlesource.com/', '')
- full_path = full_path.sub(/\.git\z/, '')
- full_path, _, project_path = full_path.rpartition('/')
- group = Group.find_by_full_path(full_path) || create_group_with_parents(user, full_path)
+ user = User.admins.first
- params = {
- import_url: url,
- namespace_id: group.id,
- path: project_path,
- name: project_path,
- description: FFaker::Lorem.sentence,
- visibility_level: Gitlab::VisibilityLevel.values.sample
- }
+ project_urls.each_with_index do |url, i|
+ full_path = url.sub('https://android.googlesource.com/', '')
+ full_path = full_path.sub(/\.git\z/, '')
+ full_path, _, project_path = full_path.rpartition('/')
+ group = Group.find_by_full_path(full_path) || create_group_with_parents(user, full_path)
- project = Projects::CreateService.new(user, params).execute
- project.send(:_run_after_commit_queue)
+ params = {
+ import_url: url,
+ namespace_id: group.id,
+ path: project_path,
+ name: project_path,
+ description: FFaker::Lorem.sentence,
+ visibility_level: Gitlab::VisibilityLevel.values.sample
+ }
- if project.valid?
- print '.'
- else
- print 'F'
+ project = Projects::CreateService.new(user, params).execute
+ project.send(:_run_after_commit_queue)
+
+ if project.valid?
+ print '.'
+ else
+ print 'F'
+ end
end
+ else
+ puts "Skipped. Use the `#{flag}` environment variable to enable."
end
end
end
diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
index 94c0a6845d5..67a0d3b53eb 100644
--- a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
+++ b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
@@ -1,6 +1,6 @@
# rubocop:disable all
class ConvertClosedToStateInIssue < ActiveRecord::Migration
- include Gitlab::Database
+ include Gitlab::Database::MigrationHelpers
def up
execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
diff --git a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
index 64a9c761352..307fc6a023d 100644
--- a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
+++ b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
@@ -1,6 +1,6 @@
# rubocop:disable all
class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration
- include Gitlab::Database
+ include Gitlab::Database::MigrationHelpers
def up
execute "UPDATE #{table_name} SET state = 'merged' WHERE closed = #{true_value} AND merged = #{true_value}"
diff --git a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
index 41508c2dc95..d12703cf3b2 100644
--- a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
+++ b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
@@ -1,6 +1,6 @@
# rubocop:disable all
class ConvertClosedToStateInMilestone < ActiveRecord::Migration
- include Gitlab::Database
+ include Gitlab::Database::MigrationHelpers
def up
execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
diff --git a/db/migrate/20130315124931_user_color_scheme.rb b/db/migrate/20130315124931_user_color_scheme.rb
index 06e28a49d9d..09af928fde7 100644
--- a/db/migrate/20130315124931_user_color_scheme.rb
+++ b/db/migrate/20130315124931_user_color_scheme.rb
@@ -1,6 +1,6 @@
# rubocop:disable all
class UserColorScheme < ActiveRecord::Migration
- include Gitlab::Database
+ include Gitlab::Database::MigrationHelpers
def up
add_column :users, :color_scheme_id, :integer, null: false, default: 1
diff --git a/db/migrate/20131112220935_add_visibility_level_to_projects.rb b/db/migrate/20131112220935_add_visibility_level_to_projects.rb
index 5efc17b228e..86d73753adc 100644
--- a/db/migrate/20131112220935_add_visibility_level_to_projects.rb
+++ b/db/migrate/20131112220935_add_visibility_level_to_projects.rb
@@ -1,6 +1,6 @@
# rubocop:disable all
class AddVisibilityLevelToProjects < ActiveRecord::Migration
- include Gitlab::Database
+ include Gitlab::Database::MigrationHelpers
def self.up
add_column :projects, :visibility_level, :integer, :default => 0, :null => false
diff --git a/db/migrate/20140313092127_migrate_already_imported_projects.rb b/db/migrate/20140313092127_migrate_already_imported_projects.rb
index f2e91fe1b40..0afc26b8764 100644
--- a/db/migrate/20140313092127_migrate_already_imported_projects.rb
+++ b/db/migrate/20140313092127_migrate_already_imported_projects.rb
@@ -1,6 +1,6 @@
# rubocop:disable all
class MigrateAlreadyImportedProjects < ActiveRecord::Migration
- include Gitlab::Database
+ include Gitlab::Database::MigrationHelpers
def up
execute("UPDATE projects SET import_status = 'finished' WHERE imported = #{true_value}")
diff --git a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb
index 688d8578478..0c14f75c154 100644
--- a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb
+++ b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb
@@ -1,6 +1,6 @@
# rubocop:disable all
class AddVisibilityLevelToSnippet < ActiveRecord::Migration
- include Gitlab::Database
+ include Gitlab::Database::MigrationHelpers
def up
add_column :snippets, :visibility_level, :integer, :default => 0, :null => false
diff --git a/db/migrate/20151209144329_migrate_ci_web_hooks.rb b/db/migrate/20151209144329_migrate_ci_web_hooks.rb
index cb1e556623a..62a6d334f04 100644
--- a/db/migrate/20151209144329_migrate_ci_web_hooks.rb
+++ b/db/migrate/20151209144329_migrate_ci_web_hooks.rb
@@ -1,6 +1,6 @@
# rubocop:disable all
class MigrateCiWebHooks < ActiveRecord::Migration
- include Gitlab::Database
+ include Gitlab::Database::MigrationHelpers
def up
execute(
diff --git a/db/migrate/20151209145909_migrate_ci_emails.rb b/db/migrate/20151209145909_migrate_ci_emails.rb
index 6b7a106814d..5de7b205fb1 100644
--- a/db/migrate/20151209145909_migrate_ci_emails.rb
+++ b/db/migrate/20151209145909_migrate_ci_emails.rb
@@ -1,6 +1,6 @@
# rubocop:disable all
class MigrateCiEmails < ActiveRecord::Migration
- include Gitlab::Database
+ include Gitlab::Database::MigrationHelpers
def up
# This inserts a new service: BuildsEmailService
diff --git a/db/migrate/20151210125232_migrate_ci_slack_service.rb b/db/migrate/20151210125232_migrate_ci_slack_service.rb
index 633d5148d97..fff130b7b10 100644
--- a/db/migrate/20151210125232_migrate_ci_slack_service.rb
+++ b/db/migrate/20151210125232_migrate_ci_slack_service.rb
@@ -1,6 +1,6 @@
# rubocop:disable all
class MigrateCiSlackService < ActiveRecord::Migration
- include Gitlab::Database
+ include Gitlab::Database::MigrationHelpers
def up
properties_query = 'SELECT properties FROM ci_services ' \
diff --git a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb
index dae084ce180..824f6f84195 100644
--- a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb
+++ b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb
@@ -1,6 +1,6 @@
# rubocop:disable all
class MigrateCiHipChatService < ActiveRecord::Migration
- include Gitlab::Database
+ include Gitlab::Database::MigrationHelpers
def up
# From properties strip `hipchat_` key
diff --git a/db/migrate/20160713222618_add_usage_ping_to_application_settings.rb b/db/migrate/20160713222618_add_usage_ping_to_application_settings.rb
new file mode 100644
index 00000000000..a7f76cc626e
--- /dev/null
+++ b/db/migrate/20160713222618_add_usage_ping_to_application_settings.rb
@@ -0,0 +1,9 @@
+class AddUsagePingToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :usage_ping_enabled, :boolean, default: true, null: false
+ end
+end
diff --git a/db/migrate/20161007073613_create_user_activities.rb b/db/migrate/20161007073613_create_user_activities.rb
new file mode 100644
index 00000000000..1d694e777a1
--- /dev/null
+++ b/db/migrate/20161007073613_create_user_activities.rb
@@ -0,0 +1,7 @@
+class CreateUserActivities < ActiveRecord::Migration
+ DOWNTIME = false
+
+ # This migration is a no-op. It just exists to match EE.
+ def change
+ end
+end
diff --git a/db/migrate/20170307125949_add_last_activity_on_to_users.rb b/db/migrate/20170307125949_add_last_activity_on_to_users.rb
new file mode 100644
index 00000000000..0100836b473
--- /dev/null
+++ b/db/migrate/20170307125949_add_last_activity_on_to_users.rb
@@ -0,0 +1,9 @@
+class AddLastActivityOnToUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :users, :last_activity_on, :date
+ end
+end
diff --git a/db/migrate/20170328010804_add_uuid_to_application_settings.rb b/db/migrate/20170328010804_add_uuid_to_application_settings.rb
new file mode 100644
index 00000000000..5dfcc751c7b
--- /dev/null
+++ b/db/migrate/20170328010804_add_uuid_to_application_settings.rb
@@ -0,0 +1,16 @@
+class AddUuidToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column :application_settings, :uuid, :string
+ execute("UPDATE application_settings SET uuid = #{quote(SecureRandom.uuid)}")
+ end
+
+ def down
+ remove_column :application_settings, :uuid
+ end
+end
diff --git a/db/migrate/20170407135259_add_foreigh_key_trigger_requests_trigger.rb b/db/migrate/20170407135259_add_foreigh_key_trigger_requests_trigger.rb
new file mode 100644
index 00000000000..81761c65a9f
--- /dev/null
+++ b/db/migrate/20170407135259_add_foreigh_key_trigger_requests_trigger.rb
@@ -0,0 +1,15 @@
+class AddForeighKeyTriggerRequestsTrigger < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(:ci_trigger_requests, :ci_triggers, column: :trigger_id)
+ end
+
+ def down
+ remove_foreign_key(:ci_trigger_requests, column: :trigger_id)
+ end
+end
diff --git a/db/migrate/20170410133135_add_version_field_to_markdown_cache.rb b/db/migrate/20170410133135_add_version_field_to_markdown_cache.rb
new file mode 100644
index 00000000000..d9209fe5770
--- /dev/null
+++ b/db/migrate/20170410133135_add_version_field_to_markdown_cache.rb
@@ -0,0 +1,25 @@
+class AddVersionFieldToMarkdownCache < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ %i[
+ abuse_reports
+ appearances
+ application_settings
+ broadcast_messages
+ issues
+ labels
+ merge_requests
+ milestones
+ namespaces
+ notes
+ projects
+ releases
+ snippets
+ ].each do |table|
+ add_column table, :cached_markdown_version, :integer, limit: 4
+ end
+ end
+end
diff --git a/db/migrate/20170418103908_delete_orphan_notification_settings.rb b/db/migrate/20170418103908_delete_orphan_notification_settings.rb
new file mode 100644
index 00000000000..e4b9cf65936
--- /dev/null
+++ b/db/migrate/20170418103908_delete_orphan_notification_settings.rb
@@ -0,0 +1,24 @@
+class DeleteOrphanNotificationSettings < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute("DELETE FROM notification_settings WHERE EXISTS (SELECT true FROM (#{orphan_notification_settings}) AS ns WHERE ns.id = notification_settings.id)")
+ end
+
+ def down
+ # This is a no-op method to make the migration reversible.
+ # If someone is trying to rollback for other reasons, we should not throw an Exception.
+ # raise ActiveRecord::IrreversibleMigration
+ end
+
+ def orphan_notification_settings
+ <<-SQL
+ SELECT notification_settings.id
+ FROM notification_settings
+ LEFT OUTER JOIN namespaces
+ ON namespaces.id = notification_settings.source_id
+ WHERE notification_settings.source_type = 'Namespace'
+ AND namespaces.id IS NULL
+ SQL
+ end
+end
diff --git a/db/migrate/20170419001229_add_index_to_system_note_metadata.rb b/db/migrate/20170419001229_add_index_to_system_note_metadata.rb
new file mode 100644
index 00000000000..c68fd920fff
--- /dev/null
+++ b/db/migrate/20170419001229_add_index_to_system_note_metadata.rb
@@ -0,0 +1,17 @@
+class AddIndexToSystemNoteMetadata < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # MySQL automatically creates an index on a foreign-key constraint; PostgreSQL does not
+ add_concurrent_index :system_note_metadata, :note_id, unique: true if Gitlab::Database.postgresql?
+ end
+
+ def down
+ remove_concurrent_index :system_note_metadata, :note_id, unique: true if Gitlab::Database.postgresql?
+ end
+end
diff --git a/db/post_migrate/20161128170531_drop_user_activities_table.rb b/db/post_migrate/20161128170531_drop_user_activities_table.rb
new file mode 100644
index 00000000000..00bc0c73015
--- /dev/null
+++ b/db/post_migrate/20161128170531_drop_user_activities_table.rb
@@ -0,0 +1,9 @@
+class DropUserActivitiesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # This migration is a no-op. It just exists to match EE.
+ def change
+ end
+end
diff --git a/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
index 2dd14ee5a78..04bf89c9687 100644
--- a/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
+++ b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
@@ -1,6 +1,5 @@
class MigrateBuildEventsToPipelineEvents < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
- include Gitlab::Database
DOWNTIME = false
diff --git a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb
new file mode 100644
index 00000000000..9ad36482c8a
--- /dev/null
+++ b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb
@@ -0,0 +1,87 @@
+class MigrateUserActivitiesToUsersLastActivityOn < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ USER_ACTIVITY_SET_KEY = 'user/activities'.freeze
+ ACTIVITIES_PER_PAGE = 100
+ TIME_WHEN_ACTIVITY_SET_WAS_INTRODUCED = Time.utc(2016, 12, 1)
+
+ def up
+ return if activities_count(TIME_WHEN_ACTIVITY_SET_WAS_INTRODUCED, Time.now).zero?
+
+ day = Time.at(activities(TIME_WHEN_ACTIVITY_SET_WAS_INTRODUCED, Time.now).first.second)
+
+ transaction do
+ while day <= Time.now.utc.tomorrow
+ persist_last_activity_on(day: day)
+ day = day.tomorrow
+ end
+ end
+ end
+
+ def down
+ # This ensures we don't lock all users for the duration of the migration.
+ update_column_in_batches(:users, :last_activity_on, nil) do |table, query|
+ query.where(table[:last_activity_on].not_eq(nil))
+ end
+ end
+
+ private
+
+ def persist_last_activity_on(day:, page: 1)
+ activities_count = activities_count(day.at_beginning_of_day, day.at_end_of_day)
+
+ return if activities_count.zero?
+
+ activities = activities(day.at_beginning_of_day, day.at_end_of_day, page: page)
+
+ update_sql =
+ Arel::UpdateManager.new(ActiveRecord::Base).
+ table(users_table).
+ set(users_table[:last_activity_on] => day.to_date).
+ where(users_table[:username].in(activities.map(&:first))).
+ to_sql
+
+ connection.exec_update(update_sql, self.class.name, [])
+
+ unless last_page?(page, activities_count)
+ persist_last_activity_on(day: day, page: page + 1)
+ end
+ end
+
+ def users_table
+ @users_table ||= Arel::Table.new(:users)
+ end
+
+ def activities(from, to, page: 1)
+ Gitlab::Redis.with do |redis|
+ redis.zrangebyscore(USER_ACTIVITY_SET_KEY, from.to_i, to.to_i,
+ with_scores: true,
+ limit: limit(page))
+ end
+ end
+
+ def activities_count(from, to)
+ Gitlab::Redis.with do |redis|
+ redis.zcount(USER_ACTIVITY_SET_KEY, from.to_i, to.to_i)
+ end
+ end
+
+ def limit(page)
+ [offset(page), ACTIVITIES_PER_PAGE]
+ end
+
+ def total_pages(count)
+ (count.to_f / ACTIVITIES_PER_PAGE).ceil
+ end
+
+ def last_page?(page, count)
+ page >= total_pages(count)
+ end
+
+ def offset(page)
+ (page - 1) * ACTIVITIES_PER_PAGE
+ end
+end
diff --git a/db/post_migrate/20170406142253_migrate_user_project_view.rb b/db/post_migrate/20170406142253_migrate_user_project_view.rb
new file mode 100644
index 00000000000..22f0f2ac200
--- /dev/null
+++ b/db/post_migrate/20170406142253_migrate_user_project_view.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateUserProjectView < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ update_column_in_batches(:users, :project_view, 2) do |table, query|
+ query.where(table[:project_view].eq(0))
+ end
+ end
+
+ def down
+ # Nothing can be done to restore old values
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3422847d729..290d969d7de 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,8 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170408033905) do
+ActiveRecord::Schema.define(version: 20170419001229) do
+
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_trgm"
@@ -23,6 +24,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.datetime "created_at"
t.datetime "updated_at"
t.text "message_html"
+ t.integer "cached_markdown_version"
end
create_table "appearances", force: :cascade do |t|
@@ -33,6 +35,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "description_html"
+ t.integer "cached_markdown_version"
end
create_table "application_settings", force: :cascade do |t|
@@ -115,6 +118,9 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.integer "unique_ips_limit_time_window"
t.boolean "unique_ips_limit_enabled", default: false, null: false
t.decimal "polling_interval_multiplier", default: 1.0, null: false
+ t.integer "cached_markdown_version"
+ t.boolean "usage_ping_enabled", default: true, null: false
+ t.string "uuid"
end
create_table "audit_events", force: :cascade do |t|
@@ -158,6 +164,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.string "color"
t.string "font"
t.text "message_html"
+ t.integer "cached_markdown_version"
end
create_table "chat_names", force: :cascade do |t|
@@ -476,6 +483,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.integer "time_estimate"
t.integer "relative_position"
t.datetime "closed_at"
+ t.integer "cached_markdown_version"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -540,6 +548,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.text "description_html"
t.string "type"
t.integer "group_id"
+ t.integer "cached_markdown_version"
end
add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
@@ -660,6 +669,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.text "title_html"
t.text "description_html"
t.integer "time_estimate"
+ t.integer "cached_markdown_version"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -697,6 +707,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.text "title_html"
t.text "description_html"
t.date "start_date"
+ t.integer "cached_markdown_version"
end
add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
@@ -723,6 +734,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.integer "parent_id"
t.boolean "require_two_factor_authentication", default: false, null: false
t.integer "two_factor_grace_period", default: 48, null: false
+ t.integer "cached_markdown_version"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
@@ -757,6 +769,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.integer "resolved_by_id"
t.string "discussion_id"
t.text "note_html"
+ t.integer "cached_markdown_version"
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
@@ -953,6 +966,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.integer "auto_cancel_pending_pipelines", default: 0, null: false
t.boolean "printing_merge_request_link_enabled", default: true, null: false
t.string "import_jid"
+ t.integer "cached_markdown_version"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1025,6 +1039,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.datetime "created_at"
t.datetime "updated_at"
t.text "description_html"
+ t.integer "cached_markdown_version"
end
add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
@@ -1096,6 +1111,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.integer "visibility_level", default: 0, null: false
t.text "title_html"
t.text "content_html"
+ t.integer "cached_markdown_version"
end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
@@ -1139,6 +1155,8 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.datetime "updated_at", null: false
end
+ add_index "system_note_metadata", ["note_id"], name: "index_system_note_metadata_on_note_id", unique: true, using: :btree
+
create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
@@ -1300,6 +1318,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.string "organization"
t.boolean "authorized_projects_populated"
t.boolean "ghost"
+ t.date "last_activity_on"
t.boolean "notified_of_own_activity"
t.boolean "require_two_factor_authentication_from_group", default: false, null: false
t.integer "two_factor_grace_period", default: 48, null: false
@@ -1356,6 +1375,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
+ add_foreign_key "ci_trigger_requests", "ci_triggers", column: "trigger_id", name: "fk_b8ec8b7245", on_delete: :cascade
add_foreign_key "ci_trigger_schedules", "ci_triggers", column: "trigger_id", name: "fk_90a406cc94", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "container_repositories", "projects"
diff --git a/doc/README.md b/doc/README.md
index b0b3d2156a7..fb393aa09a1 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -6,10 +6,6 @@ All technical content published by GitLab lives in the documentation, including:
- [User docs](#user-documentation): general documentation dedicated to regular users of GitLab
- [Admin docs](#administrator-documentation): general documentation dedicated to administrators of GitLab instances
- [Contributor docs](#contributor-documentation): general documentation on how to develop and contribute to GitLab
-- [Topics](topics/index.md): pages organized per topic, gathering all the
- resources already published by GitLab related to a specific subject, including
- general docs, [technical articles](development/writing_documentation.md#technical-articles),
- blog posts and video tutorials.
- [GitLab University](university/README.md): guides to learn Git and GitLab
through courses and videos.
@@ -19,6 +15,7 @@ All technical content published by GitLab lives in the documentation, including:
- [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
+- [Discussions](user/discussions/index.md) Threads, comments, and resolvable discussions in issues, commits, and merge requests.
- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
@@ -62,6 +59,7 @@ All technical content published by GitLab lives in the documentation, including:
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint.
- [Operations](administration/operations.md) Keeping GitLab up and running.
+- [Polling](administration/polling.md) Configure how often the GitLab UI polls for updates
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
- [Reply by email](administration/reply_by_email.md) Allow users to comment on issues and merge requests by replying to notification emails.
- [Repository checks](administration/repository_checks.md) Periodic Git repository checks.
@@ -72,6 +70,7 @@ All technical content published by GitLab lives in the documentation, including:
- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs.
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
+- [User cohorts](user/admin_area/user_cohorts.md) View user activity over time.
- [Web terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab.
- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index b4e7bf21e35..4638a9c9782 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -492,7 +492,7 @@ which ideally should not have Redis or Sentinels on it for a HA setup.
redis['master_name'] = 'gitlab-redis'
## The same password for Redis authentication you set up for the master node.
- redis['password'] = 'redis-password-goes-here'
+ redis['master_password'] = 'redis-password-goes-here'
## A list of sentinels with `host` and `port`
gitlab_rails['redis_sentinels'] = [
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index 6515b1a264a..b21817c1fd3 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -1,6 +1,6 @@
# PlantUML & GitLab
-> [Introduced][ce-7810] in GitLab 8.16.
+> [Introduced][ce-8537] in GitLab 8.16.
When [PlantUML](http://plantuml.com) integration is enabled and configured in
GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents
@@ -28,7 +28,7 @@ using Tomcat:
sudo apt-get install tomcat7
sudo cp target/plantuml.war /var/lib/tomcat7/webapps/plantuml.war
sudo chown tomcat7:tomcat7 /var/lib/tomcat7/webapps/plantuml.war
-sudo service restart tomcat7
+sudo service tomcat7 restart
```
Once the Tomcat service restarts the PlantUML service will be ready and
@@ -93,3 +93,5 @@ Some parameters can be added to the AsciiDoc block definition:
- *height*: Height attribute added to the img tag.
Markdown does not support any parameters and will always use PNG format.
+
+[ce-8537]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8537 \ No newline at end of file
diff --git a/doc/administration/polling.md b/doc/administration/polling.md
new file mode 100644
index 00000000000..35aaa20df2c
--- /dev/null
+++ b/doc/administration/polling.md
@@ -0,0 +1,24 @@
+# Polling configuration
+
+The GitLab UI polls for updates for different resources (issue notes, issue
+titles, pipeline statuses, etc.) on a schedule appropriate to the resource.
+
+In "Application settings -> Real-time features" you can configure "Polling
+interval multiplier". This multiplier is applied to all resources at once,
+and decimal values are supported. For the sake of the examples below, we will
+say that issue notes poll every 2 seconds, and issue titles poll every 5
+seconds; these are _not_ the actual values.
+
+- 1 is the default, and recommended for most installations. (Issue notes poll
+every 2 seconds, and issue titles poll every 5 seconds.)
+- 0 will disable UI polling completely. (On the next poll, clients will stop
+polling for updates.)
+- A value greater than 1 will slow polling down. If you see issues with
+database load from lots of clients polling for updates, increasing the
+multiplier from 1 can be a good compromise, rather than disabling polling
+completely. (For example: If this is set to 2, then issue notes poll every 4
+seconds, and issue titles poll every 10 seconds.)
+- A value between 0 and 1 will make the UI poll more frequently (so updates
+will show in other sessions faster), but is **not recommended**. 1 should be
+fast enough. (For example, if this is set to 0.5, then issue notes poll every
+1 second, and issue titles poll every 2.5 seconds.)
diff --git a/doc/api/README.md b/doc/api/README.md
index e627b6f2ee8..d444ce94573 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -303,6 +303,17 @@ Additional pagination headers are also sent back.
| `X-Next-Page` | The index of the next page |
| `X-Prev-Page` | The index of the previous page |
+## Namespaced path encoding
+
+If using namespaced API calls, make sure that the `NAMESPACE/PROJECT_NAME` is
+URL-encoded.
+
+For example, `/` is represented by `%2F`:
+
+```
+/api/v4/projects/diaspora%2Fdiaspora
+```
+
## `id` vs `iid`
When you work with the API, you may notice two similar fields in API entities:
@@ -398,7 +409,6 @@ Content-Type: application/json
}
```
-
## Clients
There are many unofficial GitLab API Clients for most of the popular
diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md
index 96b8d654c58..21de7d18632 100644
--- a/doc/api/access_requests.md
+++ b/doc/api/access_requests.md
@@ -25,7 +25,7 @@ GET /projects/:id/access_requests
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The group/project ID or path |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests
@@ -66,7 +66,7 @@ POST /projects/:id/access_requests
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The group/project ID or path |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests
@@ -97,7 +97,7 @@ PUT /projects/:id/access_requests/:user_id/approve
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The group/project ID or path |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `user_id` | integer | yes | The user ID of the access requester |
| `access_level` | integer | no | A valid access level (defaults: `30`, developer access level) |
@@ -130,7 +130,7 @@ DELETE /projects/:id/access_requests/:user_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The group/project ID or path |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `user_id` | integer | yes | The user ID of the access requester |
```bash
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index f57928d3c93..5f3adcc397a 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -23,7 +23,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable |
```bash
@@ -83,7 +83,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable |
| `award_id` | integer | yes | The ID of the award emoji |
@@ -126,7 +126,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable |
| `name` | string | yes | The name of the emoji, without colons |
@@ -152,7 +152,7 @@ Example Response:
"updated_at": "2016-06-17T17:47:29.266Z",
"awardable_id": 80,
"awardable_type": "Issue"
-}
+}
```
### Delete an award emoji
@@ -170,7 +170,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of an issue |
| `award_id` | integer | yes | The ID of a award_emoji |
@@ -195,7 +195,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of an issue |
| `note_id` | integer | yes | The ID of an note |
@@ -237,7 +237,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of an issue |
| `note_id` | integer | yes | The ID of a note |
| `award_id` | integer | yes | The ID of the award emoji |
@@ -277,7 +277,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of an issue |
| `note_id` | integer | yes | The ID of a note |
| `name` | string | yes | The name of the emoji, without colons |
@@ -320,7 +320,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of an issue |
| `note_id` | integer | yes | The ID of a note |
| `award_id` | integer | yes | The ID of a award_emoji |
diff --git a/doc/api/boards.md b/doc/api/boards.md
index b2106463639..17d2be0ee16 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -15,7 +15,7 @@ GET /projects/:id/boards
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/boards
@@ -71,7 +71,7 @@ GET /projects/:id/boards/:board_id/lists
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
```bash
@@ -122,7 +122,7 @@ GET /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id`| integer | yes | The ID of a board's list |
@@ -154,7 +154,7 @@ POST /projects/:id/boards/:board_id/lists
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `label_id` | integer | yes | The ID of a label |
@@ -186,7 +186,7 @@ PUT /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list |
| `position` | integer | yes | The position of the list |
@@ -219,7 +219,7 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list |
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 815aabda8e3..5717215deb6 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -12,7 +12,7 @@ GET /projects/:id/repository/branches
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches
@@ -59,7 +59,7 @@ GET /projects/:id/repository/branches/:branch
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `branch` | string | yes | The name of the branch |
```bash
@@ -109,7 +109,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `branch` | string | yes | The name of the branch |
| `developers_can_push` | boolean | no | Flag if developers can push to the branch |
| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch |
@@ -157,7 +157,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `branch` | string | yes | The name of the branch |
Example response:
@@ -195,7 +195,7 @@ POST /projects/:id/repository/branches
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `branch` | string | yes | The name of the branch |
| `ref` | string | yes | The branch name or commit SHA to create branch from |
@@ -238,7 +238,7 @@ DELETE /projects/:id/repository/branches/:branch
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `branch` | string | yes | The name of the branch |
In case of an error, an explaining message is provided.
@@ -257,7 +257,7 @@ DELETE /projects/:id/repository/merged_branches
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
index 1c26e9b33ab..9218902e84a 100644
--- a/doc/api/build_variables.md
+++ b/doc/api/build_variables.md
@@ -10,7 +10,7 @@ GET /projects/:id/variables
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables"
@@ -39,7 +39,7 @@ GET /projects/:id/variables/:key
| Attribute | Type | required | Description |
|-----------|---------|----------|-----------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `key` | string | yes | The `key` of a variable |
```
@@ -63,7 +63,7 @@ POST /projects/:id/variables
| Attribute | Type | required | Description |
|-----------|---------|----------|-----------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
| `value` | string | yes | The `value` of a variable |
@@ -88,7 +88,7 @@ PUT /projects/:id/variables/:key
| Attribute | Type | required | Description |
|-----------|---------|----------|-------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `key` | string | yes | The `key` of a variable |
| `value` | string | yes | The `value` of a variable |
@@ -113,7 +113,7 @@ DELETE /projects/:id/variables/:key
| Attribute | Type | required | Description |
|-----------|---------|----------|-------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `key` | string | yes | The `key` of a variable |
```
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 24c402346b1..9cb58dd3ae9 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -10,7 +10,7 @@ GET /projects/:id/repository/commits
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
| `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
| `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
@@ -68,7 +68,7 @@ POST /projects/:id/repository/commits
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `branch` | string | yes | The name of a branch |
| `commit_message` | string | yes | Commit message |
| `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. |
@@ -155,7 +155,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
@@ -203,7 +203,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
| `sha` | string | yes | The commit hash |
| `branch` | string | yes | The name of the branch |
@@ -245,7 +245,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
@@ -281,7 +281,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
@@ -330,7 +330,7 @@ POST /projects/:id/repository/commits/:sha/comments
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
| `sha` | string | yes | The commit SHA or name of a repository branch or tag |
| `note` | string | yes | The text of the comment |
| `path` | string | no | The file path relative to the repository |
@@ -375,7 +375,7 @@ GET /projects/:id/repository/commits/:sha/statuses
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
| `sha` | string | yes | The commit SHA
| `ref` | string | no | The name of a repository branch or tag or, if not given, the default branch
| `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test`
@@ -449,7 +449,7 @@ POST /projects/:id/statuses/:sha
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
| `sha` | string | yes | The commit SHA
| `state` | string | yes | The state of the status. Can be one of the following: `pending`, `running`, `success`, `failed`, `canceled`
| `ref` | string | no | The `ref` (branch or tag) to which the status refers
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index f051f55ac3e..c3fe7f84ef2 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -43,7 +43,7 @@ GET /projects/:id/deploy_keys
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/deploy_keys"
@@ -82,7 +82,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `key_id` | integer | yes | The ID of the deploy key |
```bash
@@ -114,7 +114,7 @@ POST /projects/:id/deploy_keys
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `title` | string | yes | New deploy key's title |
| `key` | string | yes | New deploy key |
| `can_push` | boolean | no | Can deploy key push to the project's repository |
@@ -145,7 +145,7 @@ DELETE /projects/:id/deploy_keys/:key_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `key_id` | integer | yes | The ID of the deploy key |
```bash
@@ -162,7 +162,7 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitla
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `key_id` | integer | yes | The ID of the deploy key |
Example response:
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index 76e18c8a9bd..0273c819614 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -10,7 +10,7 @@ GET /projects/:id/deployments
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/deployments"
@@ -147,7 +147,7 @@ GET /projects/:id/deployments/:deployment_id
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `deployment_id` | integer | yes | The ID of the deployment |
```bash
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
index 3f0a8d989f9..49930f01945 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -10,7 +10,7 @@ GET /projects/:id/environments
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/environments
@@ -41,7 +41,7 @@ POST /projects/:id/environment
| Attribute | Type | Required | Description |
| ------------- | ------- | -------- | ---------------------------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the environment |
| `external_url` | string | no | Place to link to for this environment |
@@ -72,7 +72,7 @@ PUT /projects/:id/environments/:environments_id
| Attribute | Type | Required | Description |
| --------------- | ------- | --------------------------------- | ------------------------------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `environment_id` | integer | yes | The ID of the environment | The ID of the environment |
| `name` | string | no | The new name of the environment |
| `external_url` | string | no | The new external_url |
@@ -102,7 +102,7 @@ DELETE /projects/:id/environments/:environment_id
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `environment_id` | integer | yes | The ID of the environment |
```bash
@@ -119,7 +119,7 @@ POST /projects/:id/environments/:environment_id/stop
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `environment_id` | integer | yes | The ID of the environment |
```bash
diff --git a/doc/api/groups.md b/doc/api/groups.md
index dfc6b80bfd9..bc61bfec9b9 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -53,7 +53,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or path of a group |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `archived` | boolean | no | Limit by archived status |
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
@@ -119,7 +119,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or path of a group |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4
@@ -299,7 +299,7 @@ POST /groups/:id/projects/:project_id
Parameters:
-- `id` (required) - The ID or path of a group
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
- `project_id` (required) - The ID or path of a project
## Update group
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 54c099d4bf8..5f01fcdd396 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -111,7 +111,7 @@ GET /groups/:id/issues?search=issue+title+or+description
| Attribute | Type | Required | Description |
|-------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------------------|
-| `id` | integer | yes | The ID of a group |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `iids` | Array[integer] | no | Return only the issues having the given `iid` |
@@ -193,7 +193,7 @@ GET /projects/:id/issues?search=issue+title+or+description
| Attribute | Type | Required | Description |
|-------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `iids` | Array[integer] | no | Return only the milestone having the given `iid` |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
@@ -266,7 +266,7 @@ GET /projects/:id/issues/:issue_iid
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
@@ -331,7 +331,7 @@ POST /projects/:id/issues
| Attribute | Type | Required | Description |
|-------------------------------------------|---------|----------|--------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `title` | string | yes | The title of an issue |
| `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
@@ -391,7 +391,7 @@ PUT /projects/:id/issues/:issue_iid
| Attribute | Type | Required | Description |
|----------------|---------|----------|------------------------------------------------------------------------------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `title` | string | no | The title of an issue |
| `description` | string | no | The description of an issue |
@@ -450,7 +450,7 @@ DELETE /projects/:id/issues/:issue_iid
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
@@ -472,7 +472,7 @@ POST /projects/:id/issues/:issue_iid/move
| Attribute | Type | Required | Description |
|-----------------|---------|----------|--------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `to_project_id` | integer | yes | The ID of the new project |
@@ -528,7 +528,7 @@ POST /projects/:id/issues/:issue_iid/subscribe
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
@@ -583,7 +583,7 @@ POST /projects/:id/issues/:issue_iid/unsubscribe
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
@@ -602,7 +602,7 @@ POST /projects/:id/issues/:issue_iid/todo
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
@@ -693,7 +693,7 @@ POST /projects/:id/issues/:issue_iid/time_estimate
| Attribute | Type | Required | Description |
|-------------|---------|----------|------------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
@@ -722,7 +722,7 @@ POST /projects/:id/issues/:issue_iid/reset_time_estimate
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
@@ -750,7 +750,7 @@ POST /projects/:id/issues/:issue_iid/add_spent_time
| Attribute | Type | Required | Description |
|-------------|---------|----------|------------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
@@ -779,7 +779,7 @@ POST /projects/:id/issues/:issue_iid/reset_spent_time
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
@@ -805,7 +805,7 @@ GET /projects/:id/issues/:issue_iid/time_stats
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 7340123e09d..bea2b96c97a 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -10,7 +10,7 @@ GET /projects/:id/jobs
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided |
```
@@ -125,7 +125,7 @@ GET /projects/:id/pipeline/:pipeline_id/jobs
| Attribute | Type | Required | Description |
|---------------|--------------------------------|----------|----------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_id` | integer | yes | The ID of a pipeline |
| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided |
@@ -241,7 +241,7 @@ GET /projects/:id/jobs/:job_id
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `job_id` | integer | yes | The ID of a job |
```
@@ -309,7 +309,7 @@ GET /projects/:id/jobs/:job_id/artifacts
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `job_id` | integer | yes | The ID of a job |
```
@@ -340,7 +340,7 @@ Parameters
| Attribute | Type | Required | Description |
|-------------|---------|----------|-------------------------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `ref_name` | string | yes | The ref from a repository |
| `job` | string | yes | The name of the job |
@@ -369,7 +369,7 @@ GET /projects/:id/jobs/:job_id/trace
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
-| id | integer | yes | The ID of a project |
+| id | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| job_id | integer | yes | The ID of a job |
```
@@ -393,7 +393,7 @@ POST /projects/:id/jobs/:job_id/cancel
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `job_id` | integer | yes | The ID of a job |
```
@@ -439,7 +439,7 @@ POST /projects/:id/jobs/:job_id/retry
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `job_id` | integer | yes | The ID of a job |
```
@@ -487,7 +487,7 @@ Parameters
| Attribute | Type | Required | Description |
|-------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `job_id` | integer | yes | The ID of a job |
Example of request
@@ -537,7 +537,7 @@ Parameters
| Attribute | Type | Required | Description |
|-------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `job_id` | integer | yes | The ID of a job |
Example request:
@@ -585,7 +585,7 @@ POST /projects/:id/jobs/:job_id/play
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `job_id` | integer | yes | The ID of a job |
```
diff --git a/doc/api/labels.md b/doc/api/labels.md
index 839000a4f48..778348ea371 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -10,7 +10,7 @@ GET /projects/:id/labels
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/labels
@@ -88,7 +88,7 @@ POST /projects/:id/labels
| Attribute | Type | Required | Description |
| ------------- | ------- | -------- | ---------------------------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the label |
| `color` | string | yes | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) |
| `description` | string | no | The description of the label |
@@ -124,7 +124,7 @@ DELETE /projects/:id/labels
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the label |
```bash
@@ -142,7 +142,7 @@ PUT /projects/:id/labels
| Attribute | Type | Required | Description |
| --------------- | ------- | --------------------------------- | ------------------------------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the existing label |
| `new_name` | string | yes if `color` is not provided | The new name of the label |
| `color` | string | yes if `new_name` is not provided | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) |
@@ -182,7 +182,7 @@ POST /projects/:id/labels/:label_id/subscribe
| Attribute | Type | Required | Description |
| ---------- | ----------------- | -------- | ------------------------------------ |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `label_id` | integer or string | yes | The ID or title of a project's label |
```bash
@@ -217,7 +217,7 @@ POST /projects/:id/labels/:label_id/unsubscribe
| Attribute | Type | Required | Description |
| ---------- | ----------------- | -------- | ------------------------------------ |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `label_id` | integer or string | yes | The ID or title of a project's label |
```bash
diff --git a/doc/api/members.md b/doc/api/members.md
index fe46f8f84bc..3c661284f11 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -23,7 +23,7 @@ GET /projects/:id/members
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The group/project ID or path |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `query` | string | no | A query string to search for members |
```bash
@@ -65,7 +65,7 @@ GET /projects/:id/members/:user_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The group/project ID or path |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `user_id` | integer | yes | The user ID of the member |
```bash
@@ -98,7 +98,7 @@ POST /projects/:id/members
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The group/project ID or path |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `user_id` | integer | yes | The user ID of the new member |
| `access_level` | integer | yes | A valid access level |
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
@@ -132,7 +132,7 @@ PUT /projects/:id/members/:user_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The group/project ID or path |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `user_id` | integer | yes | The user ID of the member |
| `access_level` | integer | yes | A valid access level |
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
@@ -166,7 +166,7 @@ DELETE /projects/:id/members/:user_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The group/project ID or path |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `user_id` | integer | yes | The user ID of the member |
```bash
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 2e0545da1c4..ff956add348 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -15,7 +15,7 @@ GET /projects/:id/merge_requests?iids[]=42&iids[]=43
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `iid` (optional) - Return the request having the given `iid`
- `state` (optional) - Return `all` requests or just those that are `merged`, `opened` or `closed`
- `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
@@ -87,7 +87,7 @@ GET /projects/:id/merge_requests/:merge_request_iid
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The internal ID of the merge request
```json
@@ -155,7 +155,7 @@ GET /projects/:id/merge_requests/:merge_request_iid/commits
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The internal ID of the merge request
@@ -192,7 +192,7 @@ GET /projects/:id/merge_requests/:merge_request_iid/changes
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The internal ID of the merge request
```json
@@ -271,7 +271,7 @@ POST /projects/:id/merge_requests
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | string | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `source_branch` | string | yes | The source branch |
| `target_branch` | string | yes | The target branch |
| `title` | string | yes | Title of MR |
@@ -347,7 +347,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | string | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The ID of a merge request |
| `target_branch` | string | no | The target branch |
| `title` | string | no | Title of MR |
@@ -422,9 +422,9 @@ Only for admins and project owners. Soft deletes the merge request in question.
DELETE /projects/:id/merge_requests/:merge_request_iid
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
@@ -450,7 +450,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid/merge
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - Internal ID of MR
- `merge_commit_message` (optional) - Custom merge commit message
- `should_remove_source_branch` (optional) - if `true` removes the source branch
@@ -524,7 +524,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_s
```
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - Internal ID of MR
```json
@@ -596,7 +596,7 @@ GET /projects/:id/merge_requests/:merge_request_iid/closes_issues
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
@@ -671,7 +671,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/subscribe
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
@@ -745,7 +745,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/unsubscribe
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
@@ -819,7 +819,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/todo
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
@@ -1027,7 +1027,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/time_estimate
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
@@ -1056,7 +1056,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/reset_time_estimate
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request |
```bash
@@ -1084,7 +1084,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/add_spent_time
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
@@ -1113,7 +1113,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/reset_spent_time
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request |
```bash
@@ -1139,7 +1139,7 @@ GET /projects/:id/merge_requests/:merge_request_iid/time_stats
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index 3c86357a6c3..7640eeb8d00 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -17,7 +17,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `iids` | Array[integer] | optional | Return only the milestones having the given `iids` |
| `state` | string | optional | Return only `active` or `closed` milestones` |
| `search` | string | optional | Return only milestones with a title or description matching the provided string |
@@ -56,8 +56,8 @@ GET /projects/:id/milestones/:milestone_id
Parameters:
-- `id` (required) - The ID of a project
-- `milestone_id` (required) - The ID of a project milestone
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+- `milestone_id` (required) - The ID of the project's milestone
## Create new milestone
@@ -69,7 +69,7 @@ POST /projects/:id/milestones
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `title` (required) - The title of an milestone
- `description` (optional) - The description of the milestone
- `due_date` (optional) - The due date of the milestone
@@ -85,7 +85,7 @@ PUT /projects/:id/milestones/:milestone_id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `milestone_id` (required) - The ID of a project milestone
- `title` (optional) - The title of a milestone
- `description` (optional) - The description of a milestone
@@ -103,7 +103,7 @@ GET /projects/:id/milestones/:milestone_id/issues
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `milestone_id` (required) - The ID of a project milestone
## Get all merge requests assigned to a single milestone
@@ -116,5 +116,5 @@ GET /projects/:id/milestones/:milestone_id/merge_requests
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `milestone_id` (required) - The ID of a project milestone
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 5e927143714..b71fea5fc9f 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -14,7 +14,7 @@ GET /projects/:id/issues/:issue_iid/notes
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `issue_iid` (required) - The IID of an issue
```json
@@ -68,7 +68,7 @@ GET /projects/:id/issues/:issue_iid/notes/:note_id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `issue_iid` (required) - The IID of a project issue
- `note_id` (required) - The ID of an issue note
@@ -83,7 +83,7 @@ POST /projects/:id/issues/:issue_iid/notes
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `issue_id` (required) - The IID of an issue
- `body` (required) - The content of a note
- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
@@ -98,7 +98,7 @@ PUT /projects/:id/issues/:issue_iid/notes/:note_id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `issue_iid` (required) - The IID of an issue
- `note_id` (required) - The ID of a note
- `body` (required) - The content of a note
@@ -115,7 +115,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The IID of an issue |
| `note_id` | integer | yes | The ID of a note |
@@ -135,7 +135,7 @@ GET /projects/:id/snippets/:snippet_id/notes
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `snippet_id` (required) - The ID of a project snippet
### Get single snippet note
@@ -148,7 +148,7 @@ GET /projects/:id/snippets/:snippet_id/notes/:note_id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `snippet_id` (required) - The ID of a project snippet
- `note_id` (required) - The ID of an snippet note
@@ -182,7 +182,7 @@ POST /projects/:id/snippets/:snippet_id/notes
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `snippet_id` (required) - The ID of a snippet
- `body` (required) - The content of a note
@@ -196,7 +196,7 @@ PUT /projects/:id/snippets/:snippet_id/notes/:note_id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `snippet_id` (required) - The ID of a snippet
- `note_id` (required) - The ID of a note
- `body` (required) - The content of a note
@@ -213,7 +213,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `snippet_id` | integer | yes | The ID of a snippet |
| `note_id` | integer | yes | The ID of a note |
@@ -233,7 +233,7 @@ GET /projects/:id/merge_requests/:merge_request_iid/notes
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The IID of a project merge request
### Get single merge request note
@@ -246,7 +246,7 @@ GET /projects/:id/merge_requests/:merge_request_iid/notes/:note_id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The IID of a project merge request
- `note_id` (required) - The ID of a merge request note
@@ -283,7 +283,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/notes
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The IID of a merge request
- `body` (required) - The content of a note
@@ -297,7 +297,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid/notes/:note_id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The IID of a merge request
- `note_id` (required) - The ID of a note
- `body` (required) - The content of a note
@@ -314,7 +314,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The IID of a merge request |
| `note_id` | integer | yes | The ID of a note |
diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md
index 50fc19f0e08..d639e8a0991 100644
--- a/doc/api/pipeline_triggers.md
+++ b/doc/api/pipeline_triggers.md
@@ -12,7 +12,7 @@ GET /projects/:id/triggers
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers"
@@ -43,7 +43,7 @@ GET /projects/:id/triggers/:trigger_id
| Attribute | Type | required | Description |
|--------------|---------|----------|--------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `trigger_id` | integer | yes | The trigger id |
```
@@ -73,7 +73,7 @@ POST /projects/:id/triggers
| Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `description` | string | yes | The trigger name |
```
@@ -103,7 +103,7 @@ PUT /projects/:id/triggers/:trigger_id
| Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `trigger_id` | integer | yes | The trigger id |
| `description` | string | no | The trigger name |
@@ -134,7 +134,7 @@ POST /projects/:id/triggers/:trigger_id/take_ownership
| Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `trigger_id` | integer | yes | The trigger id |
```
@@ -164,7 +164,7 @@ DELETE /projects/:id/triggers/:trigger_id
| Attribute | Type | required | Description |
|----------------|---------|----------|--------------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `trigger_id` | integer | yes | The trigger id |
```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 574a8bacb25..732ad8da4ac 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -10,7 +10,7 @@ GET /projects/:id/pipelines
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines"
@@ -45,7 +45,7 @@ GET /projects/:id/pipelines/:pipeline_id
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
@@ -91,7 +91,7 @@ POST /projects/:id/pipeline
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `ref` | string | yes | Reference to commit |
```
@@ -137,7 +137,7 @@ POST /projects/:id/pipelines/:pipeline_id/retry
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
@@ -173,7 +173,7 @@ Response:
}
```
-## Cancel a pipelines jobs
+## Cancel a pipelines jobs
> [Introduced][ce-5837] in GitLab 8.11
@@ -183,7 +183,7 @@ POST /projects/:id/pipelines/:pipeline_id/cancel
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index 4f6f561b83e..ff379473961 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -23,7 +23,7 @@ GET /projects/:id/snippets
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
## Single snippet
@@ -35,7 +35,7 @@ GET /projects/:id/snippets/:snippet_id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `snippet_id` (required) - The ID of a project's snippet
```json
@@ -67,7 +67,7 @@ POST /projects/:id/snippets
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `title` (required) - The title of a snippet
- `file_name` (required) - The name of a snippet file
- `code` (required) - The content of a snippet
@@ -83,7 +83,7 @@ PUT /projects/:id/snippets/:snippet_id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `snippet_id` (required) - The ID of a project's snippet
- `title` (optional) - The title of a snippet
- `file_name` (optional) - The name of a snippet file
@@ -101,7 +101,7 @@ DELETE /projects/:id/snippets/:snippet_id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `snippet_id` (required) - The ID of a project's snippet
## Snippet content
@@ -114,5 +114,5 @@ GET /projects/:id/snippets/:snippet_id/raw
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `snippet_id` (required) - The ID of a project's snippet
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 686f3dba35d..63f88a464f5 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -18,6 +18,7 @@ Constants for project visibility levels are next:
The project can be cloned without any authentication.
+
## List projects
Get a list of visible projects for authenticated user. When being accessed without authentication, all public projects are returned.
@@ -157,8 +158,7 @@ Parameters:
### Get single project
-Get a specific project, identified by project ID or NAMESPACE/PROJECT_NAME, which is owned by the authenticated user.
-If using namespaced projects call make sure that the NAMESPACE/PROJECT_NAME is URL-encoded, eg. `/api/v3/projects/diaspora%2Fdiaspora` (where `/` is represented by `%2F`). This endpoint can be accessed without authentication if
+Get a specific project. This endpoint can be accessed without authentication if
the project is publicly accessible.
```
@@ -169,7 +169,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
```json
{
@@ -295,7 +295,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
```json
[
@@ -497,7 +497,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `name` | string | yes | The name of the project |
| `path` | string | no | Custom repository name for the project. By default generated based on name |
| `default_branch` | string | no | `master` by default |
@@ -529,7 +529,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `namespace` | integer/string | yes | The ID or path of the namespace that the project will be forked to |
### Star a project
@@ -544,7 +544,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/star"
@@ -609,7 +609,7 @@ POST /projects/:id/unstar
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/unstar"
@@ -675,7 +675,7 @@ POST /projects/:id/archive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/archive"
@@ -757,7 +757,7 @@ POST /projects/:id/unarchive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/unarchive"
@@ -840,7 +840,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
## Uploads
@@ -856,7 +856,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `file` | string | yes | The file to be uploaded |
```json
@@ -887,7 +887,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `group_id` | integer | yes | The ID of the group to share with |
| `group_access` | integer | yes | The permissions level to grant the group |
| `expires_at` | string | no | Share expiration date in ISO 8601 format: 2016-09-26 |
@@ -904,7 +904,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `group_id` | integer | yes | The ID of the group |
```bash
@@ -928,7 +928,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
### Get project hook
@@ -942,7 +942,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `hook_id` | integer | yes | The ID of a project hook |
```json
@@ -975,7 +975,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `url` | string | yes | The hook URL |
| `push_events` | boolean | no | Trigger hook on push events |
| `issues_events` | boolean | no | Trigger hook on issues events |
@@ -1000,7 +1000,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `hook_id` | integer | yes | The ID of the project hook |
| `url` | string | yes | The hook URL |
| `push_events` | boolean | no | Trigger hook on push events |
@@ -1027,7 +1027,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `hook_id` | integer | yes | The ID of the project hook |
Note the JSON response differs if the hook is available or not. If the project hook
@@ -1049,7 +1049,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
```json
[
@@ -1106,7 +1106,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `branch` | string | yes | The name of the branch |
| `developers_can_push` | boolean | no | Flag if developers can push to the branch |
| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch |
@@ -1123,7 +1123,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `branch` | string | yes | The name of the branch |
### Unprotect single branch
@@ -1138,7 +1138,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `branch` | string | yes | The name of the branch |
## Admin fork relation
@@ -1155,7 +1155,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `forked_from_id` | ID | yes | The ID of the project that was forked from |
### Delete an existing forked from relationship
@@ -1168,7 +1168,7 @@ Parameter:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
## Search for projects by name
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index b1bf9ca07cc..859cbd63831 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -13,7 +13,7 @@ GET /projects/:id/repository/tree
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `path` (optional) - The path inside repository. Used to get contend of subdirectories
- `ref` (optional) - The name of a repository branch or tag or if not given the default branch
- `recursive` (optional) - Boolean value used to get a recursive tree (false by default)
@@ -84,7 +84,7 @@ GET /projects/:id/repository/blobs/:sha
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `sha` (required) - The commit or branch name
## Raw blob content
@@ -98,7 +98,7 @@ GET /projects/:id/repository/blobs/:sha/raw
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `sha` (required) - The blob SHA
## Get file archive
@@ -112,7 +112,7 @@ GET /projects/:id/repository/archive
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `sha` (optional) - The commit SHA to download defaults to the tip of the default branch
## Compare branches, tags or commits
@@ -126,7 +126,7 @@ GET /projects/:id/repository/compare
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `from` (required) - the commit SHA or branch name
- `to` (required) - the commit SHA or branch name
@@ -181,7 +181,7 @@ GET /projects/:id/repository/contributors
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
Response:
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 46f882ce937..16d362a3530 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -222,7 +222,7 @@ GET /projects/:id/runners
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners"
@@ -259,7 +259,7 @@ POST /projects/:id/runners
| Attribute | Type | Required | Description |
|-------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `runner_id` | integer | yes | The ID of a runner |
```
@@ -290,7 +290,7 @@ DELETE /projects/:id/runners/:runner_id
| Attribute | Type | Required | Description |
|-------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `runner_id` | integer | yes | The ID of a runner |
```
diff --git a/doc/api/services.md b/doc/api/services.md
index 7d4779f1137..0f42c256099 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -490,41 +490,98 @@ Remove all previously JIRA settings from a project.
DELETE /projects/:id/services/jira
```
-## Mattermost Slash Commands
+## Slack slash commands
-Ability to receive slash commands from a Mattermost chat instance.
+Ability to receive slash commands from a Slack chat instance.
-### Create/Edit Mattermost Slash Command service
+### Get Slack slash command service settings
-Set Mattermost Slash Command for a project.
+Get Slack slash command service settings for a project.
```
-PUT /projects/:id/services/mattermost-slash-commands
+GET /projects/:id/services/slack-slash-commands
+```
+
+Example response:
+
+```json
+{
+ "id": 4,
+ "title": "Slack slash commands",
+ "created_at": "2017-06-27T05:51:39-07:00",
+ "updated_at": "2017-06-27T05:51:39-07:00",
+ "active": true,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "pipeline_events": true,
+ "properties": {
+ "token": "9koXpg98eAheJpvBs5tK"
+ }
+}
+```
+
+### Create/Edit Slack slash command service
+
+Set Slack slash command for a project.
+
+```
+PUT /projects/:id/services/slack-slash-commands
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `token` | string | yes | The Mattermost token |
+| `token` | string | yes | The Slack token |
-### Delete Mattermost Slash Command service
+### Delete Slack slash command service
-Delete Mattermost Slash Command service for a project.
+Delete Slack slash command service for a project.
```
-DELETE /projects/:id/services/mattermost-slash-commands
+DELETE /projects/:id/services/slack-slash-commands
```
-### Get Mattermost Slash Command service settings
+## Mattermost slash commands
+
+Ability to receive slash commands from a Mattermost chat instance.
+
+### Get Mattermost slash command service settings
-Get Mattermost Slash Command service settings for a project.
+Get Mattermost slash command service settings for a project.
```
GET /projects/:id/services/mattermost-slash-commands
```
+### Create/Edit Mattermost slash command service
+
+Set Mattermost slash command for a project.
+
+```
+PUT /projects/:id/services/mattermost-slash-commands
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | yes | The Mattermost token |
+
+
+### Delete Mattermost slash command service
+
+Delete Mattermost slash command service for a project.
+
+```
+DELETE /projects/:id/services/mattermost-slash-commands
+```
+
## Pipeline-Emails
Get emails for GitLab CI pipelines.
diff --git a/doc/api/tags.md b/doc/api/tags.md
index bf350f024f5..0f6c4e6794e 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -12,7 +12,7 @@ GET /projects/:id/repository/tags
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
```json
[
@@ -53,7 +53,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `tag_name` | string | yes | The name of the tag |
```bash
@@ -93,7 +93,7 @@ POST /projects/:id/repository/tags
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `tag_name` (required) - The name of a tag
- `ref` (required) - Create tag using commit SHA, another tag name, or branch name.
- `message` (optional) - Creates annotated tag.
@@ -138,7 +138,7 @@ DELETE /projects/:id/repository/tags/:tag_name
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `tag_name` (required) - The name of a tag
@@ -153,7 +153,7 @@ POST /projects/:id/repository/tags/:tag_name/release
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `tag_name` (required) - The name of a tag
- `description` (required) - Release notes with markdown support
@@ -174,7 +174,7 @@ PUT /projects/:id/repository/tags/:tag_name/release
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `tag_name` (required) - The name of a tag
- `description` (required) - Release notes with markdown support
diff --git a/doc/api/users.md b/doc/api/users.md
index 2ada4d09c84..e7ef68cffbc 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -72,6 +72,7 @@ GET /users
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -104,6 +105,7 @@ GET /users
"organization": "",
"last_sign_in_at": null,
"confirmed_at": "2012-05-30T16:53:06.148Z",
+ "last_activity_on": "2012-05-23",
"color_scheme_id": 3,
"projects_limit": 100,
"current_sign_in_at": "2014-03-19T17:54:13Z",
@@ -130,6 +132,18 @@ For example:
GET /users?username=jack_smith
```
+You can also lookup users by external UID and provider:
+
+```
+GET /users?extern_uid=:extern_uid&provider=:provider
+```
+
+For example:
+
+```
+GET /users?extern_uid=1234567&provider=github
+```
+
You can search for users who are external with: `/users?external=true`
## Single user
@@ -196,6 +210,7 @@ Parameters:
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -320,6 +335,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -365,6 +381,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -986,3 +1003,55 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `user_id` | integer | yes | The ID of the user |
| `impersonation_token_id` | integer | yes | The ID of the impersonation token |
+
+### Get user activities (admin only)
+
+>**Note:** This API endpoint is only available on 8.15 (EE) and 9.1 (CE) and above.
+
+Get the last activity date for all users, sorted from oldest to newest.
+
+The activities that update the timestamp are:
+
+ - Git HTTP/SSH activities (such as clone, push)
+ - User logging in into GitLab
+
+By default, it shows the activity for all users in the last 6 months, but this can be
+amended by using the `from` parameter.
+
+```
+GET /user/activities
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `from` | string | no | Date string in the format YEAR-MONTH-DAY, e.g. `2016-03-11`. Defaults to 6 months ago. |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/activities
+```
+
+Example response:
+
+```json
+[
+ {
+ "username": "user1",
+ "last_activity_on": "2015-12-14",
+ "last_activity_at": "2015-12-14"
+ },
+ {
+ "username": "user2",
+ "last_activity_on": "2015-12-15",
+ "last_activity_at": "2015-12-15"
+ },
+ {
+ "username": "user3",
+ "last_activity_on": "2015-12-16",
+ "last_activity_at": "2015-12-16"
+ }
+]
+```
+
+Please note that `last_activity_at` is deprecated, please use `last_activity_on`.
diff --git a/doc/articles/index.md b/doc/articles/index.md
new file mode 100644
index 00000000000..67eab36bf2c
--- /dev/null
+++ b/doc/articles/index.md
@@ -0,0 +1,16 @@
+# Technical Articles
+
+[Technical Articles](../development/writing_documentation.md#technical-articles) are
+topic-related documentation, written with an user-friendly approach and language, aiming
+to provide the community with guidance on specific processes to achieve certain objectives.
+
+They are written by members of the GitLab Team and by
+[Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
+
+## GitLab Pages
+
+- **GitLab Pages from A to Z**
+ - [Part 1: Static sites and GitLab Pages domains](../user/project/pages/getting_started_part_one.md)
+ - [Part 2: Quick start guide - Setting up GitLab Pages](../user/project/pages/getting_started_part_two.md)
+ - [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](../user/project/pages/getting_started_part_three.md)
+ - [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](../user/project/pages/getting_started_part_four.md)
diff --git a/doc/ci/README.md b/doc/ci/README.md
index b3780a08828..c4f9a3cb573 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -110,9 +110,8 @@ Here is an collection of tutorials and guides on setting up your CI pipeline.
- [Run PHP Composer & NPM scripts then deploy them to a staging server](examples/deployment/composer-npm-deploy.md)
- **Blog posts**
- [Automated Debian packaging](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
- - [Spring boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
- - [Setting up CI for iOS projects](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
- - [Using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+ - [Spring boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
+ - [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
- [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
- [Building a new GitLab Docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
- [CI/CD with GitLab in action](https://about.gitlab.com/2017/03/13/ci-cd-demo/)
diff --git a/doc/ci/autodeploy/img/auto_deploy_dropdown.png b/doc/ci/autodeploy/img/auto_deploy_dropdown.png
index 957870ec8c7..b93b0a08fea 100644
--- a/doc/ci/autodeploy/img/auto_deploy_dropdown.png
+++ b/doc/ci/autodeploy/img/auto_deploy_dropdown.png
Binary files differ
diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md
index 4028a5efa9e..9fa2b2c4969 100644
--- a/doc/ci/autodeploy/index.md
+++ b/doc/ci/autodeploy/index.md
@@ -1,6 +1,8 @@
# Auto deploy
-> [Introduced][mr-8135] in GitLab 8.15. Currently requires a [Public project][project-settings].
+> [Introduced][mr-8135] in GitLab 8.15.
+> Auto deploy is an experimental feature and is not recommended for Production use at this time.
+> As of GitLab 9.1, access to the container registry is only available while the Pipeline is running. Restarting a pod, scaling a service, or other actions which require on-going access will fail. On-going secure access is planned for a subsequent release.
Auto deploy is an easy way to configure GitLab CI for the deployment of your
application. GitLab Community maintains a list of `.gitlab-ci.yml`
@@ -15,7 +17,8 @@ deployment.
## Supported templates
-The list of supported auto deploy templates is available [here][auto-deploy-templates].
+The list of supported auto deploy templates is available in the
+[gitlab-ci-yml project][auto-deploy-templates].
## Configuration
@@ -32,10 +35,37 @@ enable [Kubernetes service][kubernetes-service].
1. Test your deployment configuration using a [Review App][review-app] that was
created automatically for you.
+## Private Project Support
+
+> Experimental support [introduced][mr-2] in GitLab 9.1.
+
+When a project has been marked as private, GitLab's [Container Registry][container-registry] requires authentication when downloading containers. Auto deploy will automatically provide the required authentication information to Kubernetes, allowing temporary access to the registry. Authentication credentials will be valid while the pipeline is running, allowing for a successful initial deployment.
+
+After the pipeline completes, Kubernetes will no longer be able to access the container registry. Restarting a pod, scaling a service, or other actions which require on-going access to the registry will fail. On-going secure access is planned for a subsequent release.
+
+## PostgreSQL Database Support
+
+> Experimental support [introduced][mr-8] in GitLab 9.1.
+
+In order to support applications that require a database, [PostgreSQL][postgresql] is provisioned by default. Credentials to access the database are preconfigured, but can be customized by setting the associated [variables](#postgresql-variables). These credentials can be used for defining a `DATABASE_URL` of the format: `postgres://user:password@postgres-host:postgres-port/postgres-database`. It is important to note that the database itself is temporary, and contents will be not be saved.
+
+PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRES` to `"yes"`.
+
+### PostgreSQL Variables
+
+1. `DISABLE_POSTGRES: "yes"`: disable automatic deployment of PostgreSQL
+1. `POSTGRES_USER: "my-user"`: use custom username for PostgreSQL
+1. `POSTGRES_PASSWORD: "password"`: use custom password for PostgreSQL
+1. `POSTGRES_DB: "my database"`: use custom database name for PostgreSQL
+
[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
+[mr-2]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/2
+[mr-8]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/8
[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html
[project-services]: ../../user/project/integrations/project_services.md
[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy
[kubernetes-service]: ../../user/project/integrations/kubernetes.md
[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../review_apps/index.md
+[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html
+[postgresql]: https://www.postgresql.org/
diff --git a/doc/ci/img/pipelines.png b/doc/ci/img/pipelines.png
index 5937e9d99c8..a604fcb2587 100644
--- a/doc/ci/img/pipelines.png
+++ b/doc/ci/img/pipelines.png
Binary files differ
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index e380282f910..5f611314d09 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -227,3 +227,31 @@ branch of project with ID `9` every night at `00:30`:
```
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
+
+## Using scheduled triggers
+
+> [Introduced][ci-10533] in GitLab CE 9.1 as experimental.
+
+In order to schedule a trigger, navigate to your project's **Settings ➔ CI/CD Pipelines ➔ Triggers** and edit an existing trigger token.
+
+![Triggers Schedule edit](img/trigger_schedule_edit.png)
+
+To set up a scheduled trigger:
+
+1. Check the **Schedule trigger (experimental)** checkbox
+1. Enter a cron value for the frequency of the trigger ([learn more about cron notation](http://www.nncron.ru/help/EN/working/cron-format.htm))
+1. Enter the timezone of the cron trigger ([see a list of timezones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones))
+1. Enter the branch or tag that the trigger will target
+1. Hit **Save trigger** for the changes to take effect
+
+![Triggers Schedule create](img/trigger_schedule_create.png)
+
+You can check a next execution date of the scheduled trigger, which is automatically calculated by a server.
+
+![Triggers Schedule create](img/trigger_schedule_updated_next_run_at.png)
+
+> **Notes**:
+- Those triggers won't be executed precicely. Because scheduled triggers are handled by Sidekiq, which runs according to its interval. For exmaple, if you set a trigger to be executed every minute (`* * * * *`) and the Sidekiq worker performs 00:00 and 12:00 o'clock every day (`0 */12 * * *`), then your trigger will be executed only 00:00 and 12:00 o'clock every day. To change the Sidekiq worker's frequency, you have to edit the `trigger_schedule_worker` value in `config/gitlab.yml` and restart GitLab. The Sidekiq worker's configuration on GiLab.com is able to be looked up at [here](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example#L185).
+- Cron notation is parsed by [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler).
+
+[ci-10533]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533
diff --git a/doc/ci/triggers/img/trigger_schedule_create.png b/doc/ci/triggers/img/trigger_schedule_create.png
new file mode 100644
index 00000000000..3cfdc00b7a7
--- /dev/null
+++ b/doc/ci/triggers/img/trigger_schedule_create.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_schedule_edit.png b/doc/ci/triggers/img/trigger_schedule_edit.png
new file mode 100644
index 00000000000..647eac0a5d0
--- /dev/null
+++ b/doc/ci/triggers/img/trigger_schedule_edit.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_schedule_updated_next_run_at.png b/doc/ci/triggers/img/trigger_schedule_updated_next_run_at.png
new file mode 100644
index 00000000000..71d08d04c37
--- /dev/null
+++ b/doc/ci/triggers/img/trigger_schedule_updated_next_run_at.png
Binary files differ
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index bb78a0de0c5..1e81905c081 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -29,7 +29,8 @@ The table below shows what kind of documentation goes where.
| `doc/legal/` | Legal documents about contributing to GitLab. |
| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
-| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`); Technical Articles: user guides, admin guides, technical overviews, tutorials (`doc/topics/topic-name/`). |
+| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
+| `doc/articles/` | [Technical Articles](writing_documentation.md#technical-articles): user guides, admin guides, technical overviews, tutorials (`doc/articles/article-title/index.md`). |
---
@@ -61,8 +62,8 @@ The table below shows what kind of documentation goes where.
located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
1. The `doc/topics/` directory holds topic-related technical content. Create
`doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
- Note that `topics` holds the index page per topic, and technical articles. General
- user- and admin- related documentation, should be placed accordingly.
+ General user- and admin- related documentation, should be placed accordingly.
+1. For technical articles, place their images under `doc/articles/article-title/img/`.
---
diff --git a/doc/development/fe_guide/droplab/droplab.md b/doc/development/fe_guide/droplab/droplab.md
new file mode 100644
index 00000000000..8f0b6b21953
--- /dev/null
+++ b/doc/development/fe_guide/droplab/droplab.md
@@ -0,0 +1,256 @@
+# DropLab
+
+A generic dropdown for all of your custom dropdown needs.
+
+## Usage
+
+DropLab can be used by simply adding a `data-dropdown-trigger` HTML attribute.
+This attribute allows us to find the "trigger" _(toggle)_ for the dropdown,
+whether that is a button, link or input.
+
+The value of the `data-dropdown-trigger` should be a CSS selector that
+DropLab can use to find the trigger's dropdown list.
+
+You should also add the `data-dropdown` attribute to declare the dropdown list.
+The value is irrelevant.
+
+The DropLab class has no side effects, so you must always call `.init` when
+the DOM is ready. `DropLab.prototype.init` takes the same arguments as `DropLab.prototype.addHook`.
+If you do not provide any arguments, it will globally query and instantiate all droplab compatible dropdowns.
+
+```html
+<a href="#" data-dropdown-trigger="#list">Toggle</a>
+
+<ul id="list" data-dropdown>
+ <!-- ... -->
+<ul>
+```
+```js
+const droplab = new DropLab();
+droplab.init();
+```
+
+As you can see, we have a "Toggle" link, that is declared as a trigger.
+It provides a selector to find the dropdown list it should control.
+
+### Static data
+
+You can add static list items.
+
+```html
+<a href="#" data-dropdown-trigger="#list">Toggle</a>
+
+<ul id="list" data-dropdown>
+ <li>Static value 1</li>
+ <li>Static value 2</li>
+<ul>
+```
+```js
+const droplab = new DropLab();
+droplab.init();
+```
+
+### Explicit instantiation
+
+You can pass the trigger and list elements as constructor arguments to return
+a non-global instance of DropLab using the `DropLab.prototype.init` method.
+
+```html
+<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a>
+
+<ul id="list" data-dropdown>
+ <!-- ... -->
+<ul>
+```
+```js
+const trigger = document.getElementById('trigger');
+const list = document.getElementById('list');
+
+const droplab = new DropLab();
+droplab.init(trigger, list);
+```
+
+You can also add hooks to an existing DropLab instance using `DropLab.prototype.addHook`.
+
+```html
+<a href="#" data-dropdown-trigger="#auto-dropdown">Toggle</a>
+<ul id="auto-dropdown" data-dropdown><!-- ... --><ul>
+
+<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a>
+<ul id="list" data-dropdown><!-- ... --><ul>
+```
+```js
+const droplab = new DropLab();
+
+droplab.init();
+
+const trigger = document.getElementById('trigger');
+const list = document.getElementById('list');
+
+droplab.addHook(trigger, list);
+```
+
+
+### Dynamic data
+
+Adding `data-dynamic` to your dropdown element will enable dynamic list rendering.
+
+You can template a list item using the keys of the data object provided.
+Use the handlebars syntax `{{ value }}` to HTML escape the value.
+Use the `<%= value %>` syntax to simply interpolate the value.
+Use the `<%= value %>` syntax to evaluate the value.
+
+Passing an array of objects to `DropLab.prototype.addData` will render that data
+for all `data-dynamic` dropdown lists tracked by that DropLab instance.
+
+```html
+<a href="#" data-dropdown-trigger="#list">Toggle</a>
+
+<ul id="list" data-dropdown data-dynamic>
+ <li><a href="#" data-id="{{id}}">{{text}}</a></li>
+</ul>
+```
+```js
+const droplab = new DropLab();
+
+droplab.init().addData([{
+ id: 0,
+ text: 'Jacob',
+}, {
+ id: 1,
+ text: 'Jeff',
+}]);
+```
+
+Alternatively, you can specify a specific dropdown to add this data to but passing
+the data as the second argument and and the `id` of the trigger element as the first argument.
+
+```html
+<a href="#" data-dropdown-trigger="#list" id="trigger">Toggle</a>
+
+<ul id="list" data-dropdown data-dynamic>
+ <li><a href="#" data-id="{{id}}">{{text}}</a></li>
+</ul>
+```
+```js
+const droplab = new DropLab();
+
+droplab.init().addData('trigger', [{
+ id: 0,
+ text: 'Jacob',
+}, {
+ id: 1,
+ text: 'Jeff',
+}]);
+```
+
+This allows you to mix static and dynamic content with ease, even with one trigger.
+
+Note the use of scoping regarding the `data-dropdown` attribute to capture both
+dropdown lists, one of which is dynamic.
+
+```html
+<input id="trigger" data-dropdown-trigger="#list">
+<div id="list" data-dropdown>
+ <ul>
+ <li><a href="#">Static item 1</a></li>
+ <li><a href="#">Static item 2</a></li>
+ </ul>
+ <ul data-dynamic>
+ <li><a href="#" data-id="{{id}}">{{text}}</a></li>
+ </ul>
+</div>
+```
+```js
+const droplab = new DropLab();
+
+droplab.init().addData('trigger', [{
+ id: 0,
+ text: 'Jacob',
+}, {
+ id: 1,
+ text: 'Jeff',
+}]);
+```
+
+## Internal selectors
+
+DropLab adds some CSS classes to help lower the barrier to integration.
+
+For example,
+
+* The `droplab-item-selected` css class is added to items that have been selected
+either by a mouse click or by enter key selection.
+* The `droplab-item-active` css class is added to items that have been selected
+using arrow key navigation.
+
+## Internal events
+
+DropLab uses some custom events to help lower the barrier to integration.
+
+For example,
+
+* The `click.dl` event is fired when an `li` list item has been clicked. It is also
+fired when a list item has been selected with the keyboard. It is also fired when a
+`HookButton` button is clicked (a registered `button` tag or `a` tag trigger).
+* The `input.dl` event is fired when a `HookInput` (a registered `input` tag trigger) triggers an `input` event.
+* The `mousedown.dl` event is fired when a `HookInput` triggers a `mousedown` event.
+* The `keyup.dl` event is fired when a `HookInput` triggers a `keyup` event.
+* The `keydown.dl` event is fired when a `HookInput` triggers a `keydown` event.
+
+These custom events add a `detail` object to the vanilla `Event` object that provides some potentially useful data.
+
+## Plugins
+
+Plugins are objects that are registered to be executed when a hook is added (when a droplab trigger and dropdown are instantiated).
+
+If no modules API is detected, the library will fall back as it does with `window.DropLab` and will add `window.DropLab.plugins.PluginName`.
+
+### Usage
+
+To use plugins, you can pass them in an array as the third argument of `DropLab.prototype.init` or `DropLab.prototype.addHook`.
+Some plugins require configuration values, the config object can be passed as the fourth argument.
+
+```html
+<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a>
+<ul id="list" data-dropdown><!-- ... --><ul>
+```
+```js
+const droplab = new DropLab();
+
+const trigger = document.getElementById('trigger');
+const list = document.getElementById('list');
+
+droplab.init(trigger, list, [droplabAjax], {
+ droplabAjax: {
+ endpoint: '/some-endpoint',
+ method: 'setData',
+ },
+});
+```
+
+### Documentation
+
+* [Ajax plugin](plugins/ajax.md)
+* [Filter plugin](plugins/filter.md)
+* [InputSetter plugin](plugins/input_setter.md)
+
+### Development
+
+When plugins are initialised for a droplab trigger+dropdown, DropLab will
+call the plugins `init` function, so this must be implemented in the plugin.
+
+```js
+class MyPlugin {
+ static init() {
+ this.someProp = 'someProp';
+ this.someMethod();
+ }
+
+ static someMethod() {
+ this.otherProp = 'otherProp';
+ }
+}
+
+export default MyPlugin;
+```
diff --git a/doc/development/fe_guide/droplab/plugins/ajax.md b/doc/development/fe_guide/droplab/plugins/ajax.md
new file mode 100644
index 00000000000..9c7e56fa448
--- /dev/null
+++ b/doc/development/fe_guide/droplab/plugins/ajax.md
@@ -0,0 +1,37 @@
+# Ajax
+
+`Ajax` is a droplab plugin that allows for retrieving and rendering list data from a server.
+
+## Usage
+
+Add the `Ajax` object to the plugins array of a `DropLab.prototype.init` or `DropLab.prototype.addHook` call.
+
+`Ajax` requires 2 config values, the `endpoint` and `method`.
+
+* `endpoint` should be a URL to the request endpoint.
+* `method` should be `setData` or `addData`.
+* `setData` completely replaces the dropdown with the response data.
+* `addData` appends the response data to the current dropdown list.
+
+```html
+<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a>
+<ul id="list" data-dropdown><!-- ... --><ul>
+```
+```js
+ const droplab = new DropLab();
+
+ const trigger = document.getElementById('trigger');
+ const list = document.getElementById('list');
+
+ droplab.addHook(trigger, list, [Ajax], {
+ Ajax: {
+ endpoint: '/some-endpoint',
+ method: 'setData',
+ },
+ });
+```
+
+Optionally you can set `loadingTemplate` to a HTML string. This HTML string will
+replace the dropdown list whilst the request is pending.
+
+Additionally, you can set `onError` to a function to catch any XHR errors.
diff --git a/doc/development/fe_guide/droplab/plugins/filter.md b/doc/development/fe_guide/droplab/plugins/filter.md
new file mode 100644
index 00000000000..0853ea4d320
--- /dev/null
+++ b/doc/development/fe_guide/droplab/plugins/filter.md
@@ -0,0 +1,45 @@
+# Filter
+
+`Filter` is a plugin that allows for filtering data that has been added
+to the dropdown using a simple fuzzy string search of an input value.
+
+## Usage
+
+Add the `Filter` object to the plugins array of a `DropLab.prototype.init` or `DropLab.prototype.addHook` call.
+
+* `Filter` requires a config value for `template`.
+* `template` should be the key of the objects within your data array that you want to compare
+to the user input string, for filtering.
+
+```html
+<input href="#" id="trigger" data-dropdown-trigger="#list">
+<ul id="list" data-dropdown data-dynamic>
+ <li><a href="#" data-id="{{id}}">{{text}}</a></li>
+<ul>
+```
+```js
+ const droplab = new DropLab();
+
+ const trigger = document.getElementById('trigger');
+ const list = document.getElementById('list');
+
+ droplab.init(trigger, list, [Filter], {
+ Filter: {
+ template: 'text',
+ },
+ });
+
+ droplab.addData('trigger', [{
+ id: 0,
+ text: 'Jacob',
+ }, {
+ id: 1,
+ text: 'Jeff',
+ }]);
+```
+
+Above, the input string will be compared against the `test` key of the passed data objects.
+
+Optionally you can set `filterFunction` to a function. This function will be used instead
+of `Filter`'s built in string search. `filterFunction` is passed 2 arguments, the first
+is one of the data objects, the second is the current input value.
diff --git a/doc/development/fe_guide/droplab/plugins/input_setter.md b/doc/development/fe_guide/droplab/plugins/input_setter.md
new file mode 100644
index 00000000000..a549603c20d
--- /dev/null
+++ b/doc/development/fe_guide/droplab/plugins/input_setter.md
@@ -0,0 +1,60 @@
+# InputSetter
+
+`InputSetter` is a plugin that allows for udating DOM out of the scope of droplab when a list item is clicked.
+
+## Usage
+
+Add the `InputSetter` object to the plugins array of a `DropLab.prototype.init` or `DropLab.prototype.addHook` call.
+
+* `InputSetter` requires a config value for `input` and `valueAttribute`.
+* `input` should be the DOM element that you want to manipulate.
+* `valueAttribute` should be a string that is the name of an attribute on your list items that is used to get the value
+to update the `input` element with.
+
+You can also set the `InputSetter` config to an array of objects, which will allow you to update multiple elements.
+
+
+```html
+<input id="input" value="">
+<div id="div" data-selected-id=""></div>
+
+<input href="#" id="trigger" data-dropdown-trigger="#list">
+<ul id="list" data-dropdown data-dynamic>
+ <li><a href="#" data-id="{{id}}">{{text}}</a></li>
+<ul>
+```
+```js
+ const droplab = new DropLab();
+
+ const trigger = document.getElementById('trigger');
+ const list = document.getElementById('list');
+
+ const input = document.getElementById('input');
+ const div = document.getElementById('div');
+
+ droplab.init(trigger, list, [InputSetter], {
+ InputSetter: [{
+ input: input,
+ valueAttribute: 'data-id',
+ } {
+ input: div,
+ valueAttribute: 'data-id',
+ inputAttribute: 'data-selected-id',
+ }],
+ });
+
+ droplab.addData('trigger', [{
+ id: 0,
+ text: 'Jacob',
+ }, {
+ id: 1,
+ text: 'Jeff',
+ }]);
+```
+
+Above, if the second list item was clicked, it would update the `#input` element
+to have a `value` of `1`, it would also update the `#div` element's `data-selected-id` to `1`.
+
+Optionally you can set `inputAttribute` to a string that is the name of an attribute on your `input` element that you want to update.
+If you do not provide an `inputAttribute`, `InputSetter` will update the `value` of the `input` element if it is an `INPUT` element,
+or the `textContent` of the `input` element if it is not an `INPUT` element.
diff --git a/doc/development/fe_guide/img/boards_diagram.png b/doc/development/fe_guide/img/boards_diagram.png
new file mode 100644
index 00000000000..7a2cf972fd0
--- /dev/null
+++ b/doc/development/fe_guide/img/boards_diagram.png
Binary files differ
diff --git a/doc/development/fe_guide/img/vue_arch.png b/doc/development/fe_guide/img/vue_arch.png
new file mode 100644
index 00000000000..a67706c7c1e
--- /dev/null
+++ b/doc/development/fe_guide/img/vue_arch.png
Binary files differ
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index f963bffde37..e2a198f637f 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -27,6 +27,59 @@ For our currently-supported browsers, see our [requirements][requirements].
---
+## Development Process
+
+When you are assigned an issue please follow the next steps:
+
+### Divide a big feature into small Merge Requests
+1. Big Merge Request are painful to review. In order to make this process easier we
+must break a big feature into smaller ones and create a Merge Request for each step.
+1. First step is to create a branch from `master`, let's call it `new-feature`. This branch
+will be the recipient of all the smaller Merge Requests. Only this one will be merged to master.
+1. Don't do any work on this one, let's keep it synced with master.
+1. Create a new branch from `new-feature`, let's call it `new-feature-step-1`. We advise you
+to clearly identify which step the branch represents.
+1. Do the first part of the modifications in this branch. The target branch of this Merge Request
+should be `new-feature`.
+1. Once `new-feature-step-1` gets merged into `new-feature` we can continue our work. Create a new
+branch from `new-feature`, let's call it `new-feature-step-2` and repeat the process done before.
+
+```shell
+* master
+|\
+| * new-feature
+| |\
+| | * new-feature-step-1
+| |\
+| | * new-feature-step-2
+| |\
+| | * new-feature-step-3
+```
+
+**Tips**
+- Make sure `new-feature` branch is always synced with `master`: merge master frequently.
+- Do the same for the feature branch you have opened. This can be accomplished by merging `master` into `new-feature` and `new-feature` into `new-feature-step-*`
+- Avoid rewriting history.
+
+### Share your work early
+1. Before writing code guarantee your vision of the architecture is aligned with
+GitLab's architecture.
+1. Add a diagram to the issue and ask a Frontend Architecture about it.
+
+ ![Diagram of Issue Boards Architecture](img/boards_diagram.png)
+
+1. Don't take more than one week between starting work on a feature and
+sharing a Merge Request with a reviewer or a maintainer.
+
+### Vue features
+1. Follow the steps in [Vue.js Best Practices](vue.md)
+1. Follow the style guide.
+1. Only a handful of people are allowed to merge Vue related features.
+Reach out to @jschatz, @iamphill, @fatihacet or @filipa early in this process.
+
+
+---
+
## [Architecture](architecture.md)
How we go about making fundamental design decisions in GitLab's frontend team
or make changes to our frontend development guidelines.
@@ -90,3 +143,13 @@ Our accessibility standards and resources.
[scss-lint]: https://github.com/brigade/scss-lint
[install]: ../../install/installation.md#4-node
[requirements]: ../../install/requirements.md#supported-web-browsers
+
+---
+
+## [DropLab](droplab/droplab.md)
+Our internal `DropLab` dropdown library.
+
+* [DropLab](droplab/droplab.md)
+* [Ajax plugin](droplab/plugins/ajax.md)
+* [Filter plugin](droplab/plugins/filter.md)
+* [InputSetter plugin](droplab/plugins/input_setter.md)
diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md
index 9437a5f7a6e..2ddcbe13afa 100644
--- a/doc/development/fe_guide/performance.md
+++ b/doc/development/fe_guide/performance.md
@@ -12,8 +12,8 @@ Thus, we must strike a balance between sending requests and the feeling of realt
Use the following rules when creating realtime solutions.
1. The server will tell you how much to poll by sending `Poll-Interval` in the header.
-Use that as your polling interval. This way it is easy for system administrators to change the
-polling rate.
+Use that as your polling interval. This way it is [easy for system administrators to change the
+polling rate](../../administration/polling.md).
A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
1. A response with HTTP status `4XX` or `5XX` should disable polling as well.
1. Use a common library for polling.
@@ -48,8 +48,8 @@ Steps to split page-specific JavaScript from the main `main.js`:
```haml
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('lib_chart')
- = page_specific_javascript_bundle_tag('graphs')
+ = webpack_bundle_tag 'lib_chart'
+ = webpack_bundle_tag 'graphs'
```
The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js`
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index abd241c0bc8..1d2b0558948 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -58,7 +58,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
import Bar from './bar';
```
-- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
+- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
- When declaring multiple globals, always use one `/* global [name] */` line per variable.
@@ -71,6 +71,16 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
/* global Cookies */
/* global jQuery */
```
+
+- Use up to 3 parameters for a function or class. If you need more accept an Object instead.
+
+ ```javascript
+ // bad
+ fn(p1, p2, p3, p4) {}
+
+ // good
+ fn(options) {}
+ ```
#### Modules, Imports, and Exports
- Use ES module syntax to import modules
@@ -168,6 +178,23 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
- Avoid constructors with side-effects
+- Prefer `.map`, `.reduce` or `.filter` over `.forEach`
+A forEach will cause side effects, it will be mutating the array being iterated. Prefer using `.map`,
+`.reduce` or `.filter`
+
+ ```javascript
+ const users = [ { name: 'Foo' }, { name: 'Bar' } ];
+
+ // bad
+ users.forEach((user, index) => {
+ user.id = index;
+ });
+
+ // good
+ const usersWithId = users.map((user, index) => {
+ return Object.assign({}, user, { id: index });
+ });
+ ```
#### Parse Strings into Numbers
- `parseInt()` is preferable over `Number()` or `+`
@@ -183,6 +210,19 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
parseInt('10', 10);
```
+#### CSS classes used for JavaScript
+- If the class is being used in Javascript it needs to be prepend with `js-`
+ ```html
+ // bad
+ <button class="add-user">
+ Add User
+ </button>
+
+ // good
+ <button class="js-add-user">
+ Add User
+ </button>
+ ```
### Vue.js
@@ -200,6 +240,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
#### Naming
- **Extensions**: Use `.vue` extension for Vue components.
- **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances:
+
```javascript
// bad
import cardBoard from 'cardBoard';
@@ -217,6 +258,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
cardBoard: CardBoard
};
```
+
- **Props Naming:**
- Avoid using DOM component prop names.
- Use kebab-case instead of camelCase to provide props in templates.
@@ -243,12 +285,18 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
<component v-if="bar"
param="baz" />
+ <button class="btn">Click me</button>
+
// good
<component
v-if="bar"
param="baz"
/>
+ <button class="btn">
+ Click me
+ </button>
+
// if props fit in one line then keep it on the same line
<component bar="bar" />
```
diff --git a/doc/development/fe_guide/testing.md b/doc/development/fe_guide/testing.md
index 8d3513d3566..157c13352ca 100644
--- a/doc/development/fe_guide/testing.md
+++ b/doc/development/fe_guide/testing.md
@@ -13,9 +13,24 @@ for more information on general testing practices at GitLab.
## Karma test suite
GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test
-framework for our JavaScript unit tests. For tests that rely on DOM
-manipulation we use fixtures which are pre-compiled from HAML source files and
-served during testing by the [jasmine-jquery][jasmine-jquery] plugin.
+framework for our JavaScript unit tests. For tests that rely on DOM
+manipulation, we generate HTML files using RSpec suites (see `spec/javascripts/fixtures/*.rb` for examples).
+Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`).
+Those will be migrated over time.
+Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin.
+
+JavaScript tests live in `spec/javascripts/`, matching the folder structure
+of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js`
+has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file.
+
+Keep in mind that in a CI environment, these tests are run in a headless
+browser and you will not have access to certain APIs, such as
+[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
+which will have to be stubbed.
+
+### Writing tests
+### Vue.js unit tests
+See this [section][vue-test].
### Running frontend tests
@@ -80,24 +95,23 @@ If an integration test depends on JavaScript to run correctly, you need to make
sure the spec is configured to enable JavaScript when the tests are run. If you
don't do this you'll see vague error messages from the spec runner.
-To enable a JavaScript driver in an `rspec` test, add `js: true` to the
+To enable a JavaScript driver in an `rspec` test, add `:js` to the
individual spec or the context block containing multiple specs that need
JavaScript enabled:
```ruby
-
# For one spec
-it 'presents information about abuse report', js: true do
- # assertions...
+it 'presents information about abuse report', :js do
+ # assertions...
end
-describe "Admin::AbuseReports", js: true do
- it 'presents information about abuse report' do
- # assertions...
- end
- it 'shows buttons for adding to abuse report' do
- # assertions...
- end
+describe "Admin::AbuseReports", :js do
+ it 'presents information about abuse report' do
+ # assertions...
+ end
+ it 'shows buttons for adding to abuse report' do
+ # assertions...
+ end
end
```
@@ -113,13 +127,12 @@ file for the failing spec, add the `@javascript` flag above the Scenario:
```
@javascript
Scenario: Developer can approve merge request
- Given I am a "Shop" developer
- And I visit project "Shop" merge requests page
- And merge request 'Bug NS-04' must be approved
- And I click link "Bug NS-04"
- When I click link "Approve"
- Then I should see approved merge request "Bug NS-04"
-
+ Given I am a "Shop" developer
+ And I visit project "Shop" merge requests page
+ And merge request 'Bug NS-04' must be approved
+ And I click link "Bug NS-04"
+ When I click link "Approve"
+ Then I should see approved merge request "Bug NS-04"
```
[capybara]: http://teamcapybara.github.io/capybara/
@@ -127,3 +140,4 @@ Scenario: Developer can approve merge request
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/
+[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 3e3406e7d6a..73d2ffc1bdc 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -19,13 +19,31 @@ We don't want to refactor all GitLab frontend code into Vue.js, here are some gu
when not to use Vue.js:
- Adding or changing static information;
-- Features that highly depend on jQuery will be hard to work with Vue.js
+- Features that highly depend on jQuery will be hard to work with Vue.js;
+- Features without reactive data;
As always, the Frontend Architectural Experts are available to help with any Vue or JavaScript questions.
-## How to build a new feature with Vue.js
+## Vue architecture
-**Components, Stores and Services**
+All new features built with Vue.js must follow a [Flux architecture][flux].
+The main goal we are trying to achieve is to have only one data flow and only one data entry.
+In order to achieve this goal, each Vue bundle needs a Store - where we keep all the data -,
+a Service - that we use to communicate with the server - and a main Vue component.
+
+Think of the Main Vue Component as the entry point of your application. This is the only smart
+component that should exist in each Vue feature.
+This component is responsible for:
+1. Calling the Service to get data from the server
+1. Calling the Store to store the data received
+1. Mounting all the other components
+
+ ![Vue Architecture](img/vue_arch.png)
+
+You can also read about this architecture in vue docs about [state management][state-management]
+and about [one way data flow][one-way-data-flow].
+
+### Components, Stores and Services
In some features implemented with Vue.js, like the [issue board][issue-boards]
or [environments table][environments-table]
@@ -46,16 +64,17 @@ _For consistency purposes, we recommend you to follow the same structure._
Let's look into each of them:
-**A `*_bundle.js` file**
+### A `*_bundle.js` file
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
-The Store and the Service should be imported and initialized in this file and provided as a prop to the main component.
+The Store and the Service should be imported and initialized in this file and
+provided as a prop to the main component.
Don't forget to follow [these steps.][page_specific_javascript]
-**A folder for Components**
+### A folder for Components
This folder holds all components that are specific of this new feature.
If you need to use or create a component that will probably be used somewhere
@@ -70,20 +89,219 @@ in one table would not be a good use of this pattern.
You can read more about components in Vue.js site, [Component System][component-system]
-**A folder for the Store**
+### A folder for the Store
The Store is a class that allows us to manage the state in a single
-source of truth.
+source of truth. It is not aware of the service or the components.
The concept we are trying to follow is better explained by Vue documentation
itself, please read this guide: [State Management][state-management]
-**A folder for the Service**
+### A folder for the Service
+
+The Service is a class used only to communicate with the server.
+It does not store or manipulate any data. It is not aware of the store or the components.
+We use [vue-resource][vue-resource-repo] to communicate with the server.
+
+Vue Resource should only be imported in the service file.
+
+ ```javascript
+ import Vue from 'vue';
+ import VueResource from 'vue-resource';
+
+ Vue.use(VueResource);
+ ```
+
+### CSRF token
+We use a Vue Resource interceptor to manage the CSRF token.
+`app/assets/javascripts/vue_shared/vue_resource_interceptor.js` holds all our common interceptors.
+Note: You don't need to load `app/assets/javascripts/vue_shared/vue_resource_interceptor.js`
+since it's already being loaded by `common_vue.js`.
+
+### End Result
+
+The following example shows an application:
+
+```javascript
+// store.js
+export default class Store {
+
+ /**
+ * This is where we will iniatialize the state of our data.
+ * Usually in a small SPA you don't need any options when starting the store. In the case you do
+ * need guarantee it's an Object and it's documented.
+ *
+ * @param {Object} options
+ */
+ constructor(options) {
+ this.options = options;
+
+ // Create a state object to handle all our data in the same place
+ this.todos = []:
+ }
+
+ setTodos(todos = []) {
+ this.todos = todos;
+ }
+
+ addTodo(todo) {
+ this.todos.push(todo);
+ }
+
+ removeTodo(todoID) {
+ const state = this.todos;
+
+ const newState = state.filter((element) => {element.id !== todoID});
+
+ this.todos = newState;
+ }
+}
+
+// service.js
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import 'vue_shared/vue_resource_interceptor';
+
+Vue.use(VueResource);
+
+export default class Service {
+ constructor(options) {
+ this.todos = Vue.resource(endpoint.todosEndpoint);
+ }
+
+ getTodos() {
+ return this.todos.get();
+ }
+
+ addTodo(todo) {
+ return this.todos.put(todo);
+ }
+}
+// todo_component.vue
+<script>
+export default {
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ }
+}
+</script>
+<template>
+ <div>
+ <h1>
+ Title: {{data.title}}
+ </h1>
+ <p>
+ {{data.text}}
+ </p>
+ </div>
+</template>
+
+// todos_main_component.vue
+<script>
+import Store from 'store';
+import Service from 'service';
+import TodoComponent from 'todoComponent';
+export default {
+ /**
+ * Although most data belongs in the store, each component it's own state.
+ * We want to show a loading spinner while we are fetching the todos, this state belong
+ * in the component.
+ *
+ * We need to access the store methods through all methods of our component.
+ * We need to access the state of our store.
+ */
+ data() {
+ const store = new Store();
+
+ return {
+ isLoading: false,
+ store: store,
+ todos: store.todos,
+ };
+ },
+
+ components: {
+ todo: TodoComponent,
+ },
+
+ created() {
+ this.service = new Service('todos');
+
+ this.getTodos();
+ },
+
+ methods: {
+ getTodos() {
+ this.isLoading = true;
+
+ this.service.getTodos()
+ .then(response => response.json())
+ .then((response) => {
+ this.store.setTodos(response);
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ // Show an error
+ });
+ },
+
+ addTodo(todo) {
+ this.service.addTodo(todo)
+ then(response => response.json())
+ .then((response) => {
+ this.store.addTodo(response);
+ })
+ .catch(() => {
+ // Show an error
+ });
+ }
+ }
+}
+</script>
+<template>
+ <div class="container">
+ <div v-if="isLoading">
+ <i
+ class="fa fa-spin fa-spinner"
+ aria-hidden="true" />
+ </div>
+
+ <div
+ v-if="!isLoading"
+ class="js-todo-list">
+ <template v-for='todo in todos'>
+ <todo :data="todo" />
+ </template>
+
+ <button
+ @click="addTodo"
+ class="js-add-todo">
+ Add Todo
+ </button>
+ </div>
+ <div>
+</template>
+
+// bundle.js
+import todoComponent from 'todos_main_component.vue';
+
+new Vue({
+ el: '.js-todo-app',
+ components: {
+ todoComponent,
+ },
+ render: createElement => createElement('todo-component' {
+ props: {
+ someProp: [],
+ }
+ }),
+});
-The Service is used only to communicate with the server.
-It does not store or manipulate any data.
-We use [vue-resource][vue-resource-repo] to
-communicate with the server.
+```
The [issue boards service][issue-boards-service]
is a good example of this pattern.
@@ -93,6 +311,114 @@ is a good example of this pattern.
Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs)
for best practices while writing your Vue components and templates.
+## Testing Vue Components
+
+Each Vue component has a unique output. This output is always present in the render function.
+
+Although we can test each method of a Vue component individually, our goal must be to test the output
+of the render/template function, which represents the state at all times.
+
+Make use of Vue Resource Interceptors to mock data returned by the service.
+
+Here's how we would test the Todo App above:
+
+```javascript
+import component from 'todos_main_component';
+
+describe('Todos App', () => {
+ it('should render the loading state while the request is being made', () => {
+ const Component = Vue.extend(component);
+
+ const vm = new Component().$mount();
+
+ expect(vm.$el.querySelector('i.fa-spin')).toBeDefined();
+ });
+
+ describe('with data', () => {
+ // Mock the service to return data
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([{
+ title: 'This is a todo',
+ body: 'This is the text'
+ }]), {
+ status: 200,
+ }));
+ };
+
+ let vm;
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+
+ const Component = Vue.extend(component);
+
+ vm = new Component().$mount();
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+
+ it('should render todos', (done) => {
+ setTimeout(() => {
+ expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(1);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('add todo', () => {
+ let vm;
+ beforeEach(() => {
+ const Component = Vue.extend(component);
+ vm = new Component().$mount();
+ });
+ it('should add a todos', (done) => {
+ setTimeout(() => {
+ vm.$el.querySelector('.js-add-todo').click();
+
+ // Add a new interceptor to mock the add Todo request
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
+ });
+ }, 0);
+ });
+ });
+});
+```
+
+### Stubbing API responses
+[Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with
+the response we need:
+
+```javascript
+ // Mock the service to return data
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([{
+ title: 'This is a todo',
+ body: 'This is the text'
+ }]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+ it('should do something', (done) => {
+ setTimeout(() => {
+ // Test received data
+ done();
+ }, 0);
+ });
+```
+
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
@@ -100,5 +426,8 @@ for best practices while writing your Vue components and templates.
[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
+[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow
[vue-resource-repo]: https://github.com/pagekit/vue-resource
+[vue-resource-interceptor]: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
+[flux]: https://facebook.github.io/flux
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 587922d0136..3e8b709c18f 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -4,28 +4,53 @@ When writing migrations for GitLab, you have to take into account that
these will be ran by hundreds of thousands of organizations of all sizes, some with
many years of data in their database.
-In addition, having to take a server offline for a an upgrade small or big is
-a big burden for most organizations. For this reason it is important that your
-migrations are written carefully, can be applied online and adhere to the style guide below.
+In addition, having to take a server offline for a a upgrade small or big is a
+big burden for most organizations. For this reason it is important that your
+migrations are written carefully, can be applied online and adhere to the style
+guide below.
-Migrations should not require GitLab installations to be taken offline unless
-_absolutely_ necessary - see the ["What Requires Downtime?"](what_requires_downtime.md)
-page. If a migration requires downtime, this should be clearly mentioned during
-the review process, as well as being documented in the monthly release post. For
-more information, see the "Downtime Tagging" section below.
+Migrations are **not** allowed to require GitLab installations to be taken
+offline unless _absolutely necessary_. Downtime assumptions should be based on
+the behaviour of a migration when performed using PostgreSQL, as various
+operations in MySQL may require downtime without there being alternatives.
+
+When downtime is necessary the migration has to be approved by:
+
+1. The VP of Engineering
+1. A Backend Lead
+1. A Database Specialist
+
+An up-to-date list of people holding these titles can be found at
+<https://about.gitlab.com/team/>.
+
+The document ["What Requires Downtime?"](what_requires_downtime.md) specifies
+various database operations, whether they require downtime and how to
+work around that whenever possible.
When writing your migrations, also consider that databases might have stale data
-or inconsistencies and guard for that. Try to make as little assumptions as possible
-about the state of the database.
+or inconsistencies and guard for that. Try to make as few assumptions as
+possible about the state of the database.
+
+Please don't depend on GitLab-specific code since it can change in future
+versions. If needed copy-paste GitLab code into the migration to make it forward
+compatible.
+
+## Commit Guidelines
-Please don't depend on GitLab specific code since it can change in future versions.
-If needed copy-paste GitLab code into the migration to make it forward compatible.
+Each migration **must** be added in its own commit with a descriptive commit
+message. If a commit adds a migration it _should only_ include the migration and
+any corresponding changes to `db/schema.rb`. This makes it easy to revert a
+database migration without accidentally reverting other changes.
## Downtime Tagging
Every migration must specify if it requires downtime or not, and if it should
-require downtime it must also specify a reason for this. To do so, add the
-following two constants to the migration class' body:
+require downtime it must also specify a reason for this. This is required even
+if 99% of the migrations won't require downtime as this makes it easier to find
+the migrations that _do_ require downtime.
+
+To tag a migration, add the following two constants to the migration class'
+body:
* `DOWNTIME`: a boolean that when set to `true` indicates the migration requires
downtime.
@@ -50,12 +75,53 @@ from a migration class.
## Reversibility
-Your migration should be reversible. This is very important, as it should
+Your migration **must be** reversible. This is very important, as it should
be possible to downgrade in case of a vulnerability or bugs.
In your migration, add a comment describing how the reversibility of the
migration was tested.
+## Multi Threading
+
+Sometimes a migration might need to use multiple Ruby threads to speed up a
+migration. For this to work your migration needs to include the module
+`Gitlab::Database::MultiThreadedMigration`:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::Database::MultiThreadedMigration
+end
+```
+
+You can then use the method `with_multiple_threads` to perform work in separate
+threads. For example:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::Database::MultiThreadedMigration
+
+ def up
+ with_multiple_threads(4) do
+ disable_statement_timeout
+
+ # ...
+ end
+ end
+end
+```
+
+Here the call to `disable_statement_timeout` will use the connection local to
+the `with_multiple_threads` block, instead of re-using the global connection
+pool. This ensures each thread has its own connection object, and won't time
+out when trying to obtain one.
+
+**NOTE:** PostgreSQL has a maximum amount of connections that it allows. This
+limit can vary from installation to installation. As a result it's recommended
+you do not use more than 32 threads in a single migration. Usually 4-8 threads
+should be more than enough.
+
## Removing indices
When removing an index make sure to use the method `remove_concurrent_index` instead
@@ -78,7 +144,10 @@ end
## Adding indices
-If you need to add an unique index please keep in mind there is possibility of existing duplicates. If it is possible write a separate migration for handling this situation. It can be just removing or removing with overwriting all references to these duplicates depend on situation.
+If you need to add a unique index please keep in mind there is the possibility
+of existing duplicates being present in the database. This means that should
+always _first_ add a migration that removes any duplicates, before adding the
+unique index.
When adding an index make sure to use the method `add_concurrent_index` instead
of the regular `add_index` method. The `add_concurrent_index` method
@@ -90,17 +159,22 @@ so:
```ruby
class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
+
disable_ddl_transaction!
- def change
+ def up
+ add_concurrent_index :table, :column
+ end
+ def down
+ remove_index :table, :column if index_exists?(:table, :column)
end
end
```
## Adding Columns With Default Values
-When adding columns with default values you should use the method
+When adding columns with default values you must use the method
`add_column_with_default`. This method ensures the table is updated without
requiring downtime. This method is not reversible so you must manually define
the `up` and `down` methods in your migration class.
@@ -123,6 +197,9 @@ class MyMigration < ActiveRecord::Migration
end
```
+Keep in mind that this operation can easily take 10-15 minutes to complete on
+larger installations (e.g. GitLab.com). As a result you should only add default
+values if absolutely necessary.
## Integer column type
@@ -147,13 +224,15 @@ add_column(:projects, :foo, :integer, default: 10, limit: 8)
## Testing
-Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct.
+Make sure that your migration works with MySQL and PostgreSQL with data. An
+empty database does not guarantee that your migration is correct.
Make sure your migration can be reversed.
## Data migration
-Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of using plain SQL you need to quote all input manually with `quote_string` helper.
+Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of
+using plain SQL you need to quote all input manually with `quote_string` helper.
Example with Arel:
@@ -177,3 +256,17 @@ select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(i
execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})")
end
```
+
+If you need more complex logic you can define and use models local to a
+migration. For example:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+ end
+end
+```
+
+When doing so be sure to explicitly set the model's table name so it's not
+derived from the class name or namespace.
diff --git a/doc/development/polling.md b/doc/development/polling.md
index 05e19f0c515..3b34f985cd4 100644
--- a/doc/development/polling.md
+++ b/doc/development/polling.md
@@ -51,5 +51,6 @@ request path. By doing this we avoid query parameter ordering problems and make
route matching easier.
For more information see:
+- [`Poll-Interval` header](fe_guide/performance.md#realtime-components)
- [RFC 7232](https://tools.ietf.org/html/rfc7232)
- [ETag proposal](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926)
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 5bc958f5a96..ad540ec13db 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -9,52 +9,179 @@ this guide defines a rule that contradicts the thoughtbot guide, this guide
takes precedence. Some guidelines may be repeated verbatim to stress their
importance.
-## Factories
+## Definitions
+
+### Unit tests
+
+Formal definition: https://en.wikipedia.org/wiki/Unit_testing
+
+These kind of tests ensure that a single unit of code (a method) works as
+expected (given an input, it has a predictable output). These tests should be
+isolated as much as possible. For example, model methods that don't do anything
+with the database shouldn't need a DB record. Classes that don't need database
+records should use stubs/doubles as much as possible.
+
+| Code path | Tests path | Testing engine | Notes |
+| --------- | ---------- | -------------- | ----- |
+| `app/finders/` | `spec/finders/` | RSpec | |
+| `app/helpers/` | `spec/helpers/` | RSpec | |
+| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | |
+| `app/policies/` | `spec/policies/` | RSpec | |
+| `app/presenters/` | `spec/presenters/` | RSpec | |
+| `app/routing/` | `spec/routing/` | RSpec | |
+| `app/serializers/` | `spec/serializers/` | RSpec | |
+| `app/services/` | `spec/services/` | RSpec | |
+| `app/tasks/` | `spec/tasks/` | RSpec | |
+| `app/uploaders/` | `spec/uploaders/` | RSpec | |
+| `app/views/` | `spec/views/` | RSpec | |
+| `app/workers/` | `spec/workers/` | RSpec | |
+| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. |
+
+### Integration tests
+
+Formal definition: https://en.wikipedia.org/wiki/Integration_testing
+
+These kind of tests ensure that individual parts of the application work well together, without the overhead of the actual app environment (i.e. the browser). These tests should assert at the request/response level: status code, headers, body. They're useful to test permissions, redirections, what view is rendered etc.
+
+| Code path | Tests path | Testing engine | Notes |
+| --------- | ---------- | -------------- | ----- |
+| `app/controllers/` | `spec/controllers/` | RSpec | |
+| `app/mailers/` | `spec/mailers/` | RSpec | |
+| `lib/api/` | `spec/requests/api/` | RSpec | |
+| `lib/ci/api/` | `spec/requests/ci/api/` | RSpec | |
+| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. |
+
+#### About controller tests
+
+In an ideal world, controllers should be thin. However, when this is not the
+case, it's acceptable to write a system/feature test without JavaScript instead
+of a controller test. The reason is that testing a fat controller usually
+involves a lot of stubbing, things like:
-GitLab uses [factory_girl] as a test fixture replacement.
-
-- Factory definitions live in `spec/factories/`, named using the pluralization
- of their corresponding model (`User` factories are defined in `users.rb`).
-- There should be only one top-level factory definition per file.
-- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and
- should) call `create(...)` instead of `FactoryGirl.create(...)`.
-- Make use of [traits] to clean up definitions and usages.
-- When defining a factory, don't define attributes that are not required for the
- resulting record to pass validation.
-- When instantiating from a factory, don't supply attributes that aren't
- required by the test.
-- Factories don't have to be limited to `ActiveRecord` objects.
- [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
-
-[factory_girl]: https://github.com/thoughtbot/factory_girl
-[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits
-
-## JavaScript
-
-GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on
-the command line via `bundle exec karma`.
-
-- JavaScript tests live in `spec/javascripts/`, matching the folder structure
- of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js`
- has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file.
-- Haml fixtures required for JavaScript tests live in
- `spec/javascripts/fixtures`. They should contain the bare minimum amount of
- markup necessary for the test.
-
- > **Warning:** Keep in mind that a Rails view may change and
- invalidate your test, but everything will still pass because your fixture
- doesn't reflect the latest view. Because of this we encourage you to
- generate fixtures from actual rails views whenever possible.
-
-- Keep in mind that in a CI environment, these tests are run in a headless
- browser and you will not have access to certain APIs, such as
- [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
- which will have to be stubbed.
-
-[Karma]: https://github.com/karma-runner/karma
-[Jasmine]: https://github.com/jasmine/jasmine
+```ruby
+controller.instance_variable_set(:@user, user)
+```
-For more information, see the [frontend testing guide](fe_guide/testing.md).
+and use methods which are deprecated in Rails 5 ([#23768]).
+
+[#23768]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23768
+
+#### About Karma
+
+As you may have noticed, Karma is both in the Unit tests and the Integration
+tests category. That's because Karma is a tool that provides an environment to
+run JavaScript tests, so you can either run unit tests (e.g. test a single
+JavaScript method), or integration tests (e.g. test a component that is composed
+of multiple components).
+
+### System tests or Feature tests
+
+Formal definition: https://en.wikipedia.org/wiki/System_testing.
+
+These kind of tests ensure the application works as expected from a user point
+of view (aka black-box testing). These tests should test a happy path for a
+given page or set of pages, and a test case should be added for any regression
+that couldn't have been caught at lower levels with better tests (i.e. if a
+regression is found, regression tests should be added at the lowest-level
+possible).
+
+| Tests path | Testing engine | Notes |
+| ---------- | -------------- | ----- |
+| `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. |
+| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). |
+
+[Capybara]: https://github.com/teamcapybara/capybara
+[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
+[Poltergeist]: https://github.com/teamcapybara/capybara#poltergeist
+[RackTest]: https://github.com/teamcapybara/capybara#racktest
+
+#### Best practices
+
+- Create only the necessary records in the database
+- Test a happy path and a less happy path but that's it
+- Every other possible path should be tested with Unit or Integration tests
+- Test what's displayed on the page, not the internals of ActiveRecord models.
+ For instance, if you want to verify that a record was created, add
+ expectations that its attributes are displayed on the page, not that
+ `Model.count` increased by one.
+- It's ok to look for DOM elements but don't abuse it since it makes the tests
+ more brittle
+
+If we're confident that the low-level components work well (and we should be if
+we have enough Unit & Integration tests), we shouldn't need to duplicate their
+thorough testing at the System test level.
+
+It's very easy to add tests, but a lot harder to remove or improve tests, so one
+should take care of not introducing too many (slow and duplicated) specs.
+
+The reasons why we should follow these best practices are as follows:
+
+- System tests are slow to run since they spin up the entire application stack
+ in a headless browser, and even slower when they integrate a JS driver
+- When system tests run with a JavaScript driver, the tests are run in a
+ different thread than the application. This means it does not share a
+ database connection and your test will have to commit the transactions in
+ order for the running application to see the data (and vice-versa). In that
+ case we need to truncate the database after each spec instead of simply
+ rolling back a transaction (the faster strategy that's in use for other kind
+ of tests). This is slower than transactions, however, so we want to use
+ truncation only when necessary.
+
+### Black-box tests or End-to-end tests
+
+GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse],
+[Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces
+are configured and packaged by [GitLab Omnibus].
+
+[GitLab QA] is a tool that allows to test that all these pieces integrate well
+together by building a Docker image for a given version of GitLab Rails and
+running feature tests (i.e. using Capybara) against it.
+
+The actual test scenarios and steps are [part of GitLab Rails] so that they're
+always in-sync with the codebase.
+
+[multiple pieces]: ./architecture.md#components
+[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
+[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
+[Gitaly]: https://gitlab.com/gitlab-org/gitaly
+[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages
+[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner
+[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab
+[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa
+[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa
+
+## How to test at the correct level?
+
+As many things in life, deciding what to test at each level of testing is a
+trade-off:
+
+- Unit tests are usually cheap, and you should consider them like the basement
+ of your house: you need them to be confident that your code is behaving
+ correctly. However if you run only unit tests without integration / system tests, you might [miss] the [big] [picture]!
+- Integration tests are a bit more expensive, but don't abuse them. A feature test
+ is often better than an integration test that is stubbing a lot of internals.
+- System tests are expensive (compared to unit tests), even more if they require
+ a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed)
+ section.
+
+Another way to see it is to think about the "cost of tests", this is well
+explained [in this article][tests-cost] and the basic idea is that the cost of a
+test includes:
+
+- The time it takes to write the test
+- The time it takes to run the test every time the suite runs
+- The time it takes to understand the test
+- The time it takes to fix the test if it breaks and the underlying code is OK
+- Maybe, the time it takes to change the code to make the code testable.
+
+[miss]: https://twitter.com/ThePracticalDev/status/850748070698651649
+[big]: https://twitter.com/timbray/status/822470746773409794
+[picture]: https://twitter.com/withzombies/status/829716565834752000
+[tests-cost]: https://medium.com/table-xi/high-cost-tests-and-high-value-tests-a86e27a54df#.2ulyh3a4e
+
+## Frontend testing
+
+Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md).
## RSpec
@@ -117,53 +244,124 @@ it 'is overdue' do
end
```
-### Test speed
+### System / Feature tests
-GitLab has a massive test suite that, without parallelization, can take more
-than an hour to run. It's important that we make an effort to write tests that
-are accurate and effective _as well as_ fast.
+- Feature specs should be named `ROLE_ACTION_spec.rb`, such as
+ `user_changes_password_spec.rb`.
+- Use only one `feature` block per feature spec file.
+- Use scenario titles that describe the success and failure paths.
+- Avoid scenario titles that add no information, such as "successfully".
+- Avoid scenario titles that repeat the feature title.
-Here are some things to keep in mind regarding test performance:
+### Matchers
-- `double` and `spy` are faster than `FactoryGirl.build(...)`
-- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
-- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
- `spy`, or `double` will do. Database persistence is slow!
-- Use `create(:empty_project)` instead of `create(:project)` when you don't need
- the underlying Git repository. Filesystem operations are slow!
-- Don't mark a feature as requiring JavaScript (through `@javascript` in
- Spinach or `js: true` in RSpec) unless it's _actually_ required for the test
- to be valid. Headless browser testing is slow!
+Custom matchers should be created to clarify the intent and/or hide the
+complexity of RSpec expectations.They should be placed under
+`spec/support/matchers/`. Matchers can be placed in subfolder if they apply to
+a certain type of specs only (e.g. features, requests etc.) but shouldn't be if
+they apply to multiple type of specs.
-### Features / Integration
+### Shared contexts
-GitLab uses [rspec-rails feature specs] to test features in a browser
-environment. These are [capybara] specs running on the headless [poltergeist]
-driver.
+All shared contexts should be be placed under `spec/support/shared_contexts/`.
+Shared contexts can be placed in subfolder if they apply to a certain type of
+specs only (e.g. features, requests etc.) but shouldn't be if they apply to
+multiple type of specs.
-- Feature specs live in `spec/features/` and should be named
- `ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`.
-- Use only one `feature` block per feature spec file.
-- Use scenario titles that describe the success and failure paths.
-- Avoid scenario titles that add no information, such as "successfully."
-- Avoid scenario titles that repeat the feature title.
+Each file should include only one context and have a descriptive name, e.g.
+`spec/support/shared_contexts/controllers/githubish_import_controller_shared_context.rb`.
-[rspec-rails feature specs]: https://github.com/rspec/rspec-rails#feature-specs
-[capybara]: https://github.com/teamcapybara/capybara
-[poltergeist]: https://github.com/teampoltergeist/poltergeist
+### Shared examples
-## Spinach (feature) tests
+All shared examples should be be placed under `spec/support/shared_examples/`.
+Shared examples can be placed in subfolder if they apply to a certain type of
+specs only (e.g. features, requests etc.) but shouldn't be if they apply to
+multiple type of specs.
-GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
-for its feature/integration tests in September 2012.
+Each file should include only one context and have a descriptive name, e.g.
+`spec/support/shared_examples/controllers/githubish_import_controller_shared_example.rb`.
-As of March 2016, we are [trying to avoid adding new Spinach
-tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
-opting for [RSpec feature](#features-integration) specs.
+### Helpers
-Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
-no more than one new `step` definition. If more than that is required, the
-test should be re-implemented using RSpec instead.
+Helpers are usually modules that provide some methods to hide the complexity of
+specific RSpec examples. You can define helpers in RSpec files if they're not
+intended to be shared with other specs. Otherwise, they should be be placed
+under `spec/support/helpers/`. Helpers can be placed in subfolder if they apply
+to a certain type of specs only (e.g. features, requests etc.) but shouldn't be
+if they apply to multiple type of specs.
+
+Helpers should follow the Rails naming / namespacing convention. For instance
+`spec/support/helpers/cycle_analytics_helpers.rb` should define:
+
+```ruby
+module Spec
+ module Support
+ module Helpers
+ module CycleAnalyticsHelpers
+ def create_commit_referencing_issue(issue, branch_name: random_git_name)
+ project.repository.add_branch(user, branch_name, 'master')
+ create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
+ end
+ end
+ end
+ end
+end
+```
+
+Helpers should not change the RSpec config. For instance, the helpers module
+described above should not include:
+
+```ruby
+RSpec.configure do |config|
+ config.include Spec::Support::Helpers::CycleAnalyticsHelpers
+end
+```
+
+### Factories
+
+GitLab uses [factory_girl] as a test fixture replacement.
+
+- Factory definitions live in `spec/factories/`, named using the pluralization
+ of their corresponding model (`User` factories are defined in `users.rb`).
+- There should be only one top-level factory definition per file.
+- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and
+ should) call `create(...)` instead of `FactoryGirl.create(...)`.
+- Make use of [traits] to clean up definitions and usages.
+- When defining a factory, don't define attributes that are not required for the
+ resulting record to pass validation.
+- When instantiating from a factory, don't supply attributes that aren't
+ required by the test.
+- Factories don't have to be limited to `ActiveRecord` objects.
+ [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
+
+[factory_girl]: https://github.com/thoughtbot/factory_girl
+[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits
+
+### Fixtures
+
+All fixtures should be be placed under `spec/fixtures/`.
+
+### Config
+
+RSpec config files are files that change the RSpec config (i.e.
+`RSpec.configure do |config|` blocks). They should be placed under
+`spec/support/config/`.
+
+Each file should be related to a specific domain, e.g.
+`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc.
+
+Helpers can be included in the `spec/support/config/rspec.rb` file. If a
+helpers module applies only to a certain kind of specs, it should add modifiers
+to the `config.include` call. For instance if
+`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and
+`type: :model` specs only, you would write the following:
+
+```ruby
+RSpec.configure do |config|
+ config.include Spec::Support::Helpers::CycleAnalyticsHelpers, :lib
+ config.include Spec::Support::Helpers::CycleAnalyticsHelpers, type: :model
+end
+```
## Testing Rake Tasks
@@ -201,6 +399,77 @@ describe 'gitlab:shell rake tasks' do
end
```
+## Test speed
+
+GitLab has a massive test suite that, without [parallelization], can take hours
+to run. It's important that we make an effort to write tests that are accurate
+and effective _as well as_ fast.
+
+Here are some things to keep in mind regarding test performance:
+
+- `double` and `spy` are faster than `FactoryGirl.build(...)`
+- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
+- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
+ `spy`, or `double` will do. Database persistence is slow!
+- Use `create(:empty_project)` instead of `create(:project)` when you don't need
+ the underlying Git repository. Filesystem operations are slow!
+- Don't mark a feature as requiring JavaScript (through `@javascript` in
+ Spinach or `:js` in RSpec) unless it's _actually_ required for the test
+ to be valid. Headless browser testing is slow!
+
+[parallelization]: #test-suite-parallelization-on-the-ci
+
+### Test suite parallelization on the CI
+
+Our current CI parallelization setup is as follows:
+
+1. The `knapsack` job in the prepare stage that is supposed to ensure we have a
+ `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file:
+ - The `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file is fetched
+ from S3, if it's not here we initialize the file with `{}`.
+1. Each `rspec x y` job are run with `knapsack rspec` and should have an evenly
+ distributed share of tests:
+ - It works because the jobs have access to the
+ `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` since the "artifacts
+ from all previous stages are passed by default". [^1]
+ - the jobs set their own report path to
+ `KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`.
+ - if knapsack is doing its job, test files that are run should be listed under
+ `Report specs`, not under `Leftover specs`.
+1. The `update-knapsack` job takes all the
+ `knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`
+ files from the `rspec x y` jobs and merge them all together into a single
+ `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that is then
+ uploaded to S3.
+
+After that, the next pipeline will use the up-to-date
+`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy
+is used for Spinach tests as well.
+
+### Monitoring
+
+The GitLab test suite is [monitored] and a [public dashboard] is available for
+everyone to see. Feel free to look at the slowest test files and try to improve
+them.
+
+[monitored]: ./performance.md#rspec-profiling
+[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
+
+## Spinach (feature) tests
+
+GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
+for its feature/integration tests in September 2012.
+
+As of March 2016, we are [trying to avoid adding new Spinach
+tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
+opting for [RSpec feature](#features-integration) specs.
+
+Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
+no more than one new `step` definition. If more than that is required, the
+test should be re-implemented using RSpec instead.
+
---
[Return to Development documentation](README.md)
+
+[^1]: /ci/yaml/README.html#dependencies
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index bbcd26477f3..8da6ad684f5 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -2,7 +2,8 @@
When working with a database certain operations can be performed without taking
GitLab offline, others do require a downtime period. This guide describes
-various operations and their impact.
+various operations, their impact, and how to perform them without requiring
+downtime.
## Adding Columns
@@ -41,50 +42,156 @@ information on how to use this method.
## Dropping Columns
-On PostgreSQL you can safely remove an existing column without the need for
-downtime. When you drop a column in PostgreSQL it's not immediately removed,
-instead it is simply disabled. The data is removed on the next vacuum run.
+Removing columns is tricky because running GitLab processes may still be using
+the columns. To work around this you will need two separate merge requests and
+releases: one to ignore and then remove the column, and one to remove the ignore
+rule.
-On MySQL this operation requires downtime.
+### Step 1: Ignoring The Column
-While database wise dropping a column may be fine on PostgreSQL this operation
-still requires downtime because the application code may still be using the
-column that was removed. For example, consider the following migration:
+The first step is to ignore the column in the application code. This is
+necessary because Rails caches the columns and re-uses this cache in various
+places. This can be done by including the `IgnorableColumn` module into the
+model, followed by defining the columns to ignore. For example, to ignore
+`updated_at` in the User model you'd use the following:
```ruby
-class MyMigration < ActiveRecord::Migration
- def change
- remove_column :projects, :dummy
- end
+class User < ActiveRecord::Base
+ include IgnorableColumn
+
+ ignore_column :updated_at
end
```
-Now imagine that the GitLab instance is running and actively uses the `dummy`
-column. If we were to run the migration this would result in the GitLab instance
-producing errors whenever it tries to use the `dummy` column.
+Once added you should create a _post-deployment_ migration that removes the
+column. Both these changes should be submitted in the same merge request.
-As a result of the above downtime _is_ required when removing a column, even
-when using PostgreSQL.
+### Step 2: Removing The Ignore Rule
+
+Once the changes from step 1 have been released & deployed you can set up a
+separate merge request that removes the ignore rule. This merge request can
+simply remove the `ignore_column` line, and the `include IgnorableColumn` line
+if no other `ignore_column` calls remain.
## Renaming Columns
-Renaming columns requires downtime as running GitLab instances will continue
-using the old column name until a new version is deployed. This can result
-in the instance producing errors, which in turn can impact the user experience.
+Renaming columns the normal way requires downtime as an application may continue
+using the old column name during/after a database migration. To rename a column
+without requiring downtime we need two migrations: a regular migration, and a
+post-deployment migration. Both these migration can go in the same release.
-## Changing Column Constraints
+### Step 1: Add The Regular Migration
+
+First we need to create the regular migration. This migration should use
+`Gitlab::Database::MigrationHelpers#rename_column_concurrently` to perform the
+renaming. For example
+
+```ruby
+# A regular migration in db/migrate
+class RenameUsersUpdatedAtToUpdatedAtTimestamp < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ rename_column_concurrently :users, :updated_at, :updated_at_timestamp
+ end
+
+ def down
+ cleanup_concurrent_column_rename :users, :updated_at_timestamp, :updated_at
+ end
+end
+```
+
+This will take care of renaming the column, ensuring data stays in sync, copying
+over indexes and foreign keys, etc.
+
+**NOTE:** if a column contains 1 or more indexes that do not contain the name of
+the original column, the above procedure will fail. In this case you will first
+need to rename these indexes.
-Generally changing column constraints requires checking all rows in the table to
-see if they meet the new constraint, unless a constraint is _removed_. For
-example, changing a column that previously allowed NULL values to not allow NULL
-values requires the database to verify all existing rows.
+### Step 2: Add A Post-Deployment Migration
-The specific behaviour varies a bit between databases but in general the safest
-approach is to assume changing constraints requires downtime.
+The renaming procedure requires some cleaning up in a post-deployment migration.
+We can perform this cleanup using
+`Gitlab::Database::MigrationHelpers#cleanup_concurrent_column_rename`:
+
+```ruby
+# A post-deployment migration in db/post_migrate
+class CleanupUsersUpdatedAtRename < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_rename :users, :updated_at, :updated_at_timestamp
+ end
+
+ def down
+ rename_column_concurrently :users, :updated_at_timestamp, :updated_at
+ end
+end
+```
+
+## Changing Column Constraints
+
+Adding or removing a NOT NULL clause (or another constraint) can typically be
+done without requiring downtime. However, this does require that any application
+changes are deployed _first_. Thus, changing the constraints of a column should
+happen in a post-deployment migration.
## Changing Column Types
-This operation requires downtime.
+Changing the type of a column can be done using
+`Gitlab::Database::MigrationHelpers#change_column_type_concurrently`. This
+method works similarly to `rename_column_concurrently`. For example, let's say
+we want to change the type of `users.username` from `string` to `text`.
+
+### Step 1: Create A Regular Migration
+
+A regular migration is used to create a new column with a temporary name along
+with setting up some triggers to keep data in sync. Such a migration would look
+as follows:
+
+```ruby
+# A regular migration in db/migrate
+class ChangeUsersUsernameStringToText < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ change_column_type_concurrently :users, :username, :text
+ end
+
+ def down
+ cleanup_concurrent_column_type_change :users, :username
+ end
+end
+```
+
+### Step 2: Create A Post Deployment Migration
+
+Next we need to clean up our changes using a post-deployment migration:
+
+```ruby
+# A post-deployment migration in db/post_migrate
+class ChangeUsersUsernameStringToTextCleanup < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_type_change :users
+ end
+
+ def down
+ change_column_type_concurrently :users, :username, :string
+ end
+end
+```
+
+And that's it, we're done!
## Adding Indexes
@@ -101,12 +208,19 @@ Migrations can take advantage of this by using the method
```ruby
class MyMigration < ActiveRecord::Migration
- def change
+ def up
add_concurrent_index :projects, :column_name
end
+
+ def down
+ remove_index(:projects, :column_name) if index_exists?(:projects, :column_name)
+ end
end
```
+Note that `add_concurrent_index` can not be reversed automatically, thus you
+need to manually define `up` and `down`.
+
When running this on PostgreSQL the `CONCURRENTLY` option mentioned above is
used. On MySQL this method produces a regular `CREATE INDEX` query.
@@ -125,43 +239,54 @@ This operation is safe as there's no code using the table just yet.
## Dropping Tables
-This operation requires downtime as application code may still be using the
-table.
+Dropping tables can be done safely using a post-deployment migration, but only
+if the application no longer uses the table.
## Adding Foreign Keys
-Adding foreign keys acquires an exclusive lock on both the source and target
-tables in PostgreSQL. This requires downtime as otherwise the entire application
-grinds to a halt for the duration of the operation.
+Adding foreign keys usually works in 3 steps:
+
+1. Start a transaction
+1. Run `ALTER TABLE` to add the constraint(s)
+1. Check all existing data
-On MySQL this operation also requires downtime _unless_ foreign key checks are
-disabled. Because this means checks aren't enforced this is not ideal, as such
-one should assume MySQL also requires downtime.
+Because `ALTER TABLE` typically acquires an exclusive lock until the end of a
+transaction this means this approach would require downtime.
+
+GitLab allows you to work around this by using
+`Gitlab::Database::MigrationHelpers#add_concurrent_foreign_key`. This method
+ensures that when PostgreSQL is used no downtime is needed.
## Removing Foreign Keys
-This operation should not require downtime on both PostgreSQL and MySQL.
+This operation does not require downtime.
-## Updating Data
+## Data Migrations
-Updating data should generally be safe. The exception to this is data that's
-being migrated from one version to another while the application still produces
-data in the old version.
+Data migrations can be tricky. The usual approach to migrate data is to take a 3
+step approach:
-For example, imagine the application writes the string `'dog'` to a column but
-it really is meant to write `'cat'` instead. One might think that the following
-migration is all that is needed to solve this problem:
+1. Migrate the initial batch of data
+1. Deploy the application code
+1. Migrate any remaining data
-```ruby
-class MyMigration < ActiveRecord::Migration
- def up
- execute("UPDATE some_table SET column = 'cat' WHERE column = 'dog';")
- end
-end
-```
+Usually this works, but not always. For example, if a field's format is to be
+changed from JSON to something else we have a bit of a problem. If we were to
+change existing data before deploying application code we'll most likely run
+into errors. On the other hand, if we were to migrate after deploying the
+application code we could run into the same problems.
+
+If you merely need to correct some invalid data, then a post-deployment
+migration is usually enough. If you need to change the format of data (e.g. from
+JSON to something else) it's typically best to add a new column for the new data
+format, and have the application use that. In such a case the procedure would
+be:
-Unfortunately this is not enough. Because the application is still running and
-using the old value this may result in the table still containing rows where
-`column` is set to `dog`, even after the migration finished.
+1. Add a new column in the new format
+1. Copy over existing data to this new column
+1. Deploy the application code
+1. In a post-deployment migration, copy over any remaining data
-In these cases downtime _is_ required, even for rarely updated tables.
+In general there is no one-size-fits-all solution, therefore it's best to
+discuss these kind of migrations in a merge request to make sure they are
+implemented in the best way possible.
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index 482ec54207b..166a10293c3 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -2,7 +2,7 @@
- **General Documentation**: written by the developers responsible by creating features. Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
- **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
- - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs, in the same merge request containing code.
+ - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
## Distinction between General Documentation and Technical Articles
@@ -18,7 +18,7 @@ They are topic-related documentation, written with an user-friendly approach and
A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab.
-They live under `doc/topics/topic-name/`, and can be searched per topic, within "Indexes per Topic" pages. The topics are listed on the main [Indexes per Topic](../topics/index.md) page.
+They live under `doc/articles/article-title/index.md`, and their images should be placed under `doc/articles/article-title/img/`. Find a list of existing [technical articles](../articles/index.md) here.
#### Types of Technical Articles
diff --git a/doc/install/README.md b/doc/install/README.md
index d35709266e4..58cc7d312fd 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -20,8 +20,8 @@ the hardware requirements.
- [Docker](https://docs.gitlab.com/omnibus/docker/) - Install GitLab using Docker.
- [Installation on Google Cloud Platform](google_cloud_platform/index.md) - Install
GitLab on Google Cloud Platform using our official image.
-- [Digital Ocean and Docker](digitaloceandocker.md) - Install GitLab quickly
- on DigitalOcean using Docker.
+- Testing only! [DigitalOcean and Docker Machine](digitaloceandocker.md) -
+ Quickly test any version of GitLab on DigitalOcean using Docker Machine.
## Database
diff --git a/doc/install/digitaloceandocker.md b/doc/install/digitaloceandocker.md
index 820060a489b..8efc0530b8a 100644
--- a/doc/install/digitaloceandocker.md
+++ b/doc/install/digitaloceandocker.md
@@ -1,4 +1,7 @@
-# Digital Ocean and Docker
+# Digital Ocean and Docker Machine test environment
+
+## Warning. This guide is for quickly testing different versions of GitLab and
+## not recommended for ease of future upgrades or keeping the data you create.
## Initial setup
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 5b72c2cce07..1f61a4f67bb 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -289,9 +289,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-0-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-1-stable gitlab
-**Note:** You can change `9-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `9-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -475,7 +475,7 @@ with setting up Gitaly until you upgrade to GitLab 9.2 or later.
sudo -u git cp config.toml.example config.toml
# If you are using non-default settings you need to update config.toml
sudo -u git -H editor config.toml
-
+
# Enable Gitaly in the init script
echo 'gitaly_enabled=true' | sudo tee -a /etc/default/gitlab
diff --git a/doc/intro/README.md b/doc/intro/README.md
index 1df6a52ce8a..d52b180a076 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -13,7 +13,7 @@ Create issues, labels, milestones, cast your vote, and review issues.
- [Create a new issue](../gitlab-basics/create-issue.md)
- [Assign labels to issues](../user/project/labels.md)
-- [Use milestones as an overview of your project's tracker](../workflow/milestones.md)
+- [Use milestones as an overview of your project's tracker](../user/project/milestones/index.md)
- [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md)
## Collaborate
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 65fcfc77ab1..e680a560888 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -18,10 +18,12 @@ another is through backup restore.
To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json`
(for omnibus packages) or `/home/git/gitlab/.secret` (for installations
-from source). This file contains the database encryption key and CI secret
-variables used for two-factor authentication. If you fail to restore this
-encryption key file along with the application data backup, users with two-factor
-authentication enabled will lose access to your GitLab server.
+from source). This file contains the database encryption key,
+[CI secret variables](../ci/variables/README.md#secret-variables), and
+secret variables used for [two-factor authentication](../security/two_factor_authentication.md).
+If you fail to restore this encryption key file along with the application data
+backup, users with two-factor authentication enabled and GitLab Runners will
+lose access to your GitLab server.
## Create a backup of the GitLab system
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
new file mode 100644
index 00000000000..eafd2fd9d04
--- /dev/null
+++ b/doc/topics/authentication/index.md
@@ -0,0 +1,46 @@
+# Authentication
+
+This page gathers all the resources for the topic **Authentication** within GitLab.
+
+## GitLab users
+
+- [SSH](../../ssh/README.md)
+- [Two-Factor Authentication (2FA)](../../user/profile/account/two_factor_authentication.md#two-factor-authentication)
+- **Articles:**
+ - [Support for Universal 2nd Factor Authentication - YubiKeys](https://about.gitlab.com/2016/06/22/gitlab-adds-support-for-u2f/)
+ - [Security Webcast with Yubico](https://about.gitlab.com/2016/08/31/gitlab-and-yubico-security-webcast/)
+- **Integrations:**
+ - [GitLab as OAuth2 authentication service provider](../../integration/oauth_provider.md#introduction-to-oauth)
+
+## GitLab administrators
+
+- [LDAP (Community Edition)](../../administration/auth/ldap.md)
+- [LDAP (Enterprise Edition)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html)
+- [Enforce Two-factor Authentication (2FA)](../../security/two_factor_authentication.md#enforce-two-factor-authentication-2fa)
+- **Articles:**
+ - [Feature Highlight: LDAP Integration](https://about.gitlab.com/2014/07/10/feature-highlight-ldap-sync/)
+ - [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/ldap/debugging_ldap.html)
+- **Integrations:**
+ - [OmniAuth](../../integration/omniauth.md)
+ - [Authentiq OmniAuth Provider](../../administration/auth/authentiq.md#authentiq-omniauth-provider)
+ - [Atlassian Crowd OmniAuth Provider](../../administration/auth/crowd.md)
+ - [CAS OmniAuth Provider](../../integration/cas.md)
+ - [SAML OmniAuth Provider](../../integration/saml.md)
+ - [Okta SSO provider](../../administration/auth/okta.md)
+ - [Kerberos integration (GitLab EE)](https://docs.gitlab.com/ee/integration/kerberos.html)
+
+## API
+
+- [OAuth 2 Tokens](../../api/README.md#oauth-2-tokens)
+- [Private Tokens](../../api/README.md#private-tokens)
+- [Impersonation tokens](../../api/README.md#impersonation-tokens)
+- [GitLab as an OAuth2 provider](../../api/oauth2.md#gitlab-as-an-oauth2-provider)
+- [GitLab Runner API - Authentication](../../api/ci/runners.md#authentication)
+
+## Third-party resources
+
+- [Kanboard Plugin GitLab Authentication](https://kanboard.net/plugin/gitlab-auth)
+- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+OAuth+Plugin)
+- [Setup Gitlab CE with Active Directory authentication](https://www.caseylabs.com/setup-gitlab-ce-with-active-directory-authentication/)
+- [How to customize GitLab to support OpenID authentication](http://eric.van-der-vlist.com/blog/2013/11/23/how-to-customize-gitlab-to-support-openid-authentication/)
+- [Openshift - Configuring Authentication and User Agent](https://docs.openshift.org/latest/install_config/configuring_authentication.html#GitLab)
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
new file mode 100644
index 00000000000..b99ba317a43
--- /dev/null
+++ b/doc/topics/git/index.md
@@ -0,0 +1,61 @@
+# Git documentation
+
+Git is a [free and open source](https://git-scm.com/about/free-and-open-source)
+distributed version control system designed to handle everything from small to
+very large projects with speed and efficiency.
+
+[GitLab](https://about.gitlab.com) is a Git-based fully integrated platform for
+software development. Besides Git's functionalities, GitLab has a lot of
+powerful [features](https://about.gitlab.com/features/) to enhance your
+[workflow](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
+
+We've gathered some resources to help you to get the best from Git with GitLab.
+
+## Getting started
+
+- [Git concepts](../../university/training/user_training.md#git-concepts)
+- [Start using Git on the command line](../../gitlab-basics/start-using-git.md)
+- [Command Line basic commands](../../gitlab-basics/command-line-commands.md)
+- [GitLab Git Cheat Sheet (download)](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf)
+- **Articles:**
+ - [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/)
+ - [Eight Tips to help you work better with Git](https://about.gitlab.com/2015/02/19/8-tips-to-help-you-work-better-with-git/)
+- **Presentations:**
+ - [GLU Course: About Version Control](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit?usp=sharing)
+- **Third-party resources:**
+ - What is [Git](https://git-scm.com)
+ - [Version control](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control)
+ - [Getting Started - Git Basics](https://git-scm.com/book/en/v2/Getting-Started-Git-Basics)
+ - [Getting Started - Installing Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
+ - [Git on the Server - GitLab](https://git-scm.com/book/en/v2/Git-on-the-Server-GitLab)
+
+## Branching strategies
+
+- **Articles:**
+ - [GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/)
+- **Third-party resources:**
+ - [Git Branching - Branches in a Nutshell](https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell)
+ - [Git Branching - Branching Workflows](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows)
+
+## Advanced use
+
+- [Custom Git Hooks](../../administration/custom_hooks.md)
+- [Git Attributes](../../user/project/git_attributes.md)
+- Git Submodules: [Using Git submodules with GitLab CI](../../ci/git_submodules.md#using-git-submodules-with-gitlab-ci)
+
+## API
+
+- [Gitignore templates](../../api/templates/gitignores.md)
+
+## Git LFS
+
+- [Git LFS](../../workflow/lfs/manage_large_binaries_with_git_lfs.md)
+- [Git-Annex to Git-LFS migration guide](https://docs.gitlab.com/ee/workflow/lfs/migrate_from_git_annex_to_git_lfs.html)
+- **Articles:**
+ - [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/)
+ - [Towards a production quality open source Git LFS server](https://about.gitlab.com/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/)
+
+## General information
+
+- **Articles:**
+ - [The future of SaaS hosted Git repository pricing](https://about.gitlab.com/2016/05/11/git-repository-pricing/)
diff --git a/doc/topics/index.md b/doc/topics/index.md
index 6de13d79554..ad388dff822 100644
--- a/doc/topics/index.md
+++ b/doc/topics/index.md
@@ -7,10 +7,10 @@ you through better understanding GitLab's concepts
through our regular docs, and, when available, through articles (guides,
tutorials, technical overviews, blog posts) and videos.
-- [GitLab Installation](../install/README.md)
+- [Authentication](authentication/index.md)
- [Continuous Integration (GitLab CI)](../ci/README.md)
+- [Git](git/index.md)
+- [GitLab Installation](../install/README.md)
- [GitLab Pages](../user/project/pages/index.md)
->**Note:**
-Non-linked topics are currently under development and subjected to change.
-More topics will be available soon.
+>**Note:** More topics will be available soon.
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 0b17e4ff7c1..591d1524061 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -333,7 +333,7 @@ A [platform](https://www.meteor.com) for building javascript apps.
### Milestones
-Allow you to [organize issues](https://docs.gitlab.com/ce/workflow/milestones.html) and merge requests in GitLab into a cohesive group, optionally setting a due date. A common use is keeping track of an upcoming software version. Milestones are created per-project.
+Allow you to [organize issues](../../user/project/milestones/index.md) and merge requests in GitLab into a cohesive group, optionally setting a due date. A common use is keeping track of an upcoming software version. Milestones are created per-project.
### Mirror Repositories
diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md
index ae983dea384..1191662ee14 100644
--- a/doc/update/9.0-to-9.1.md
+++ b/doc/update/9.0-to-9.1.md
@@ -1,9 +1,5 @@
# From 9.0 to 9.1
-** TODO: **
-
-# TODO clean out 9.0-specific stuff
-
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
diff --git a/doc/update/README.md b/doc/update/README.md
index 837b31abb97..d024a809f24 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -48,6 +48,20 @@ GitLab provides official Docker images for both Community and Enterprise
editions. They are based on the Omnibus package and instructions on how to
update them are in [a separate document][omnidocker].
+## Upgrading without downtime
+
+Starting with GitLab 9.1.0 it's possible to upgrade to a newer major, minor, or patch version of GitLab
+without having to take your GitLab instance offline. However, for this to work
+there are the following requirements:
+
+1. You can only upgrade 1 minor release at a time. So from 9.1 to 9.2, not to 9.3.
+2. You have to be on the most recent patch release. For example, if 9.1.15 is the last
+ release of 9.1 then you can safely upgrade from that version to any 9.2.x version.
+ However, if you are running 9.1.14 you first need to upgrade to 9.1.15.
+2. You have to use [post-deployment
+ migrations](../development/post_deployment_migrations.md).
+3. You are using PostgreSQL. If you are using MySQL please look at the release post to see if downtime is required.
+
## Upgrading between editions
GitLab comes in two flavors: [Community Edition][ce] which is MIT licensed,
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index 1c493599cf8..f69d567eeb7 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -57,7 +57,7 @@ sudo -u git -H bundle clean
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
-sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production NODE_ENV=production
```
### 4. Update gitlab-workhorse to the corresponding version
diff --git a/doc/user/admin_area/img/cohorts.png b/doc/user/admin_area/img/cohorts.png
new file mode 100644
index 00000000000..8bae7faff07
--- /dev/null
+++ b/doc/user/admin_area/img/cohorts.png
Binary files differ
diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
new file mode 100644
index 00000000000..c3f3179d99e
--- /dev/null
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -0,0 +1,102 @@
+# Usage statistics
+
+GitLab Inc. will periodically collect information about your instance in order
+to perform various actions.
+
+All statistics are opt-in and you can always disable them from the admin panel.
+
+## Version check
+
+GitLab can inform you when an update is available and the importance of it.
+
+No information other than the GitLab version and the instance's domain name
+are collected.
+
+In the **Overview** tab you can see if your GitLab version is up to date. There
+are three cases: 1) you are up to date (green), 2) there is an update available
+(yellow) and 3) your version is vulnerable and a security fix is released (red).
+
+In any case, you will see a message informing you of the state and the
+importance of the update.
+
+If enabled, the version status will also be shown in the help page (`/help`)
+for all signed in users.
+
+## Usage ping
+
+> [Introduced][ee-557] in GitLab Enterprise Edition 8.10. More statistics
+[were added][ee-735] in GitLab Enterprise Edition
+8.12. [Moved to GitLab Community Edition][ce-23361] in 9.1.
+
+GitLab Inc. can collect non-sensitive information about how GitLab users
+use their GitLab instance upon the activation of a ping feature
+located in the admin panel (`/admin/application_settings`).
+
+You can see the **exact** JSON payload that your instance sends to GitLab
+in the "Usage statistics" section of the admin panel.
+
+Nothing qualitative is collected. Only quantitative. That means no project
+names, author names, comment bodies, names of labels, etc.
+
+The usage ping is sent in order for GitLab Inc. to have a better understanding
+of how our users use our product, and to be more data-driven when creating or
+changing features.
+
+The total number of the following is sent back to GitLab Inc.:
+
+- Comments
+- Groups
+- Users
+- Projects
+- Issues
+- Labels
+- CI builds
+- Snippets
+- Milestones
+- Todos
+- Pushes
+- Merge requests
+- Environments
+- Triggers
+- Deploy keys
+- Pages
+- Project Services
+- Projects using the Prometheus service
+- Issue Boards
+- CI Runners
+- Deployments
+- Geo Nodes
+- LDAP Groups
+- LDAP Keys
+- LDAP Users
+- LFS objects
+- Protected branches
+- Releases
+- Remote mirrors
+- Uploads
+- Web hooks
+
+Also, we track if you've installed Mattermost with GitLab.
+For example: `"mattermost_enabled":true"`.
+
+More data will be added over time. The goal of this ping is to be as light as
+possible, so it won't have any performance impact on your installation when
+the calculation is made.
+
+### Deactivate the usage ping
+
+By default, usage ping is opt-out. If you want to deactivate this feature, go to
+the Settings page of your administration panel and uncheck the Usage ping
+checkbox.
+
+## Privacy policy
+
+GitLab Inc. does **not** collect any sensitive information, like project names
+or the content of the comments. GitLab Inc. does not disclose or otherwise make
+available any of the data collected on a customer specific basis.
+
+Read more about this in the [Privacy policy](https://about.gitlab.com/privacy).
+
+[ee-557]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/557
+[ee-735]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/735
+[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361
diff --git a/doc/user/admin_area/user_cohorts.md b/doc/user/admin_area/user_cohorts.md
new file mode 100644
index 00000000000..e25e7a8bbc3
--- /dev/null
+++ b/doc/user/admin_area/user_cohorts.md
@@ -0,0 +1,37 @@
+# Cohorts
+
+> **Notes:**
+> [Introduced][ce-23361] in GitLab 9.1.
+
+As a benefit of having the [usage ping active](settings/usage_statistics.md),
+GitLab lets you analyze the users' activities of your GitLab installation.
+Under `/admin/cohorts`, when the usage ping is active, GitLab will show the
+monthly cohorts of new users and their activities over time.
+
+## Overview
+
+How do we read the user cohorts table? Let's take an example with the following
+user cohorts.
+
+![User cohort example](img/cohorts.png)
+
+For the cohort of June 2016, 163 users have been added on this server and have
+been active since this month. One month later, in July 2016, out of
+these 163 users, 155 users (or 95% of the June cohort) are still active. Two
+months later, 139 users (or 85%) are still active. 9 months later, we can see
+that only 6% of this cohort are still active.
+
+The Inactive users column shows the number of users who have been added during
+the month, but who have never actually had any activity in the instance.
+
+How do we measure the activity of users? GitLab considers a user active if:
+
+* the user signs in
+* the user has Git activity (whether push or pull).
+
+## Setup
+
+1. [Activate the usage ping](settings/usage_statistics.md)
+2. Go to `/admin/cohorts` to see the user cohorts of the server
+
+[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361
diff --git a/doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png b/doc/user/discussions/img/btn_new_issue_for_all_discussions.png
index b15447ec290..b15447ec290 100644
--- a/doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png
+++ b/doc/user/discussions/img/btn_new_issue_for_all_discussions.png
Binary files differ
diff --git a/doc/user/discussions/img/comment_type_toggle.gif b/doc/user/discussions/img/comment_type_toggle.gif
new file mode 100644
index 00000000000..b73c197b97f
--- /dev/null
+++ b/doc/user/discussions/img/comment_type_toggle.gif
Binary files differ
diff --git a/doc/user/discussions/img/discussion_comment.png b/doc/user/discussions/img/discussion_comment.png
new file mode 100644
index 00000000000..8f66d138922
--- /dev/null
+++ b/doc/user/discussions/img/discussion_comment.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/discussion_view.png b/doc/user/discussions/img/discussion_view.png
index 2ee1db2eab3..2ee1db2eab3 100644
--- a/doc/user/project/merge_requests/img/discussion_view.png
+++ b/doc/user/discussions/img/discussion_view.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/discussions_resolved.png b/doc/user/discussions/img/discussions_resolved.png
index 3fd496f6da5..3fd496f6da5 100644
--- a/doc/user/project/merge_requests/img/discussions_resolved.png
+++ b/doc/user/discussions/img/discussions_resolved.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/new_issue_for_discussion.png b/doc/user/discussions/img/new_issue_for_discussion.png
index 93c9dad8921..93c9dad8921 100644
--- a/doc/user/project/merge_requests/img/new_issue_for_discussion.png
+++ b/doc/user/discussions/img/new_issue_for_discussion.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png b/doc/user/discussions/img/only_allow_merge_if_all_discussions_are_resolved.png
index 928c7d33898..928c7d33898 100644
--- a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png
+++ b/doc/user/discussions/img/only_allow_merge_if_all_discussions_are_resolved.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png b/doc/user/discussions/img/only_allow_merge_if_all_discussions_are_resolved_msg.png
index bcdc0250d7c..bcdc0250d7c 100644
--- a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png
+++ b/doc/user/discussions/img/only_allow_merge_if_all_discussions_are_resolved_msg.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/preview_issue_for_discussion.png b/doc/user/discussions/img/preview_issue_for_discussion.png
index 2ee0653b2ba..2ee0653b2ba 100644
--- a/doc/user/project/merge_requests/img/preview_issue_for_discussion.png
+++ b/doc/user/discussions/img/preview_issue_for_discussion.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/preview_issue_for_discussions.png b/doc/user/discussions/img/preview_issue_for_discussions.png
index 3fe0a666678..3fe0a666678 100644
--- a/doc/user/project/merge_requests/img/preview_issue_for_discussions.png
+++ b/doc/user/discussions/img/preview_issue_for_discussions.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/resolve_comment_button.png b/doc/user/discussions/img/resolve_comment_button.png
index 70340108874..70340108874 100644
--- a/doc/user/project/merge_requests/img/resolve_comment_button.png
+++ b/doc/user/discussions/img/resolve_comment_button.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/resolve_discussion_button.png b/doc/user/discussions/img/resolve_discussion_button.png
index ab454f661e0..ab454f661e0 100644
--- a/doc/user/project/merge_requests/img/resolve_discussion_button.png
+++ b/doc/user/discussions/img/resolve_discussion_button.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png b/doc/user/discussions/img/resolve_discussion_issue_notice.png
index e0ee6a39ffd..e0ee6a39ffd 100644
--- a/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png
+++ b/doc/user/discussions/img/resolve_discussion_issue_notice.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/resolve_discussion_open_issue.png b/doc/user/discussions/img/resolve_discussion_open_issue.png
index 98d63278326..98d63278326 100644
--- a/doc/user/project/merge_requests/img/resolve_discussion_open_issue.png
+++ b/doc/user/discussions/img/resolve_discussion_open_issue.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
new file mode 100644
index 00000000000..c5123c06ce0
--- /dev/null
+++ b/doc/user/discussions/index.md
@@ -0,0 +1,150 @@
+# Discussions
+
+The ability to contribute conversationally is offered throughout GitLab.
+
+You can leave a comment in the following places:
+
+- issues
+- merge requests
+- snippets
+- commits
+- commit diffs
+
+The comment area supports [Markdown] and [slash commands]. One can edit their
+own comment at any time, and anyone with [Master access level][permissions] or
+higher can also a comment made by someone else.
+
+Apart from the standard comments, you also have the option to create a comment
+in the form of a resolvable or threaded discussion.
+
+## Resolvable discussions
+
+>**Notes:**
+- The main feature was [introduced][ce-5022] in GitLab 8.11.
+- Resolvable discussions can be added only to merge request diffs.
+
+Discussion resolution helps keep track of progress during planning or code review.
+Resolving comments prevents you from forgetting to address feedback and lets you
+hide discussions that are no longer relevant.
+
+!["A discussion between two people on a piece of code"][discussion-view]
+
+Comments and discussions can be resolved by anyone with at least Developer
+access to the project or the author of the merge request.
+
+### Jumping between unresolved discussions
+
+When a merge request has a large number of comments it can be difficult to track
+what remains unresolved. You can jump between unresolved discussions with the
+Jump button next to the Reply field on a discussion.
+
+You can also jump to the first unresolved discussion from the button next to the
+resolved discussions tracker.
+
+!["3/4 discussions resolved"][discussions-resolved]
+
+### Marking a comment or discussion as resolved
+
+You can mark a discussion as resolved by clicking the **Resolve discussion**
+button at the bottom of the discussion.
+
+!["Resolve discussion" button][resolve-discussion-button]
+
+Alternatively, you can mark each comment as resolved individually.
+
+!["Resolve comment" button][resolve-comment-button]
+
+### Move all unresolved discussions in a merge request to an issue
+
+> [Introduced][ce-8266] in GitLab 9.1
+
+To continue all open discussions from a merge request in a new issue, click the
+**Resolve all discussions in new issue** button.
+
+![Open new issue for all unresolved discussions](img/btn_new_issue_for_all_discussions.png)
+
+Alternatively, when your project only accepts merge requests [when all discussions
+are resolved](#only-allow-merge-requests-to-be-merged-if-all-discussions-are-resolved),
+there will be an **open an issue to resolve them later** link in the merge
+request widget.
+
+![Link in merge request widget](img/resolve_discussion_open_issue.png)
+
+This will prepare an issue with its content referring to the merge request and
+the unresolved discussions.
+
+![Issue mentioning discussions in a merge request](img/preview_issue_for_discussions.png)
+
+Hitting **Submit issue** will cause all discussions to be marked as resolved and
+add a note referring to the newly created issue.
+
+![Mark discussions as resolved notice](img/resolve_discussion_issue_notice.png)
+
+You can now proceed to merge the merge request from the UI.
+
+### Moving a single discussion to a new issue
+
+> [Introduced][ce-8266] in GitLab 9.1
+
+To create a new issue for a single discussion, you can use the **Resolve this
+discussion in a new issue** button.
+
+![Create issue for discussion](img/new_issue_for_discussion.png)
+
+This will direct you to a new issue prefilled with the content of the
+discussion, similar to the issues created for delegating multiple
+discussions at once. Saving the issue will mark the discussion as resolved and
+add a note to the merge request discussion referencing the new issue.
+
+![New issue for a single discussion](img/preview_issue_for_discussion.png)
+
+### Only allow merge requests to be merged if all discussions are resolved
+
+> [Introduced][ce-7125] in GitLab 8.14.
+
+You can prevent merge requests from being merged until all discussions are
+resolved.
+
+Navigate to your project's settings page, select the
+**Only allow merge requests to be merged if all discussions are resolved** check
+box and hit **Save** for the changes to take effect.
+
+![Only allow merge if all the discussions are resolved settings](img/only_allow_merge_if_all_discussions_are_resolved.png)
+
+From now on, you will not be able to merge from the UI until all discussions
+are resolved.
+
+![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png)
+
+## Threaded discussions
+
+> [Introduced][ce-7527] in GitLab 9.1.
+
+While resolvable discussions are only available to merge request diffs,
+discussions can also be added without a diff. You can start a specific
+discussion which will look like a thread, on issues, commits, snippets, and
+merge requests.
+
+To start a threaded discussion, click on the **Comment** button toggle dropdown,
+select **Start discussion** and click **Start discussion** when you're ready to
+post the comment.
+
+![Comment type toggle](img/comment_type_toggle.gif)
+
+This will post a comment with a single thread to allow you to discuss specific
+comments in greater detail.
+
+![Discussion comment](img/discussion_comment.png)
+
+[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
+[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
+[ce-7527]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7527
+[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
+[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
+[resolve-discussion-button]: img/resolve_discussion_button.png
+[resolve-comment-button]: img/resolve_comment_button.png
+[discussion-view]: img/discussion_view.png
+[discussions-resolved]: img/discussions_resolved.png
+[markdown]: ../markdown.md
+[slash commands]: ../project/slash_commands.md
+[permissions]: ../permissions.md
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 3122e95fc0e..637967510f3 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -7,6 +7,9 @@ project itself, the highest permission level is used.
On public and internal projects the Guest role is not enforced. All users will
be able to create issues, leave comments, and pull or download the project code.
+When a member leaves the team the all assigned Issues and Merge Requests
+will be unassigned automatically.
+
GitLab administrators receive all permissions.
To add or import a user, you can follow the [project users and members
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index 63a3d3c472e..fb69d934ae1 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -143,7 +143,7 @@ into the password field.
To disable two-factor authentication on your account (for example, if you
have lost your code generation device) you can:
* [Use a saved recovery code](#use-a-saved-recovery-code)
-* [Generate new recovery codes using SSH](#generate-new-recovery-codes-using-SSH)
+* [Generate new recovery codes using SSH](#generate-new-recovery-codes-using-ssh)
* [Ask a GitLab administrator to disable two-factor authentication on your account](#ask-a-gitlab-administrator-to-disable-two-factor-authentication-on-your-account)
### Use a saved recovery code
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index 62afd8cf247..8f6b530c033 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -5,10 +5,10 @@
Cycle Analytics measures the time it takes to go from an [idea to production] for
each project you have. This is achieved by not only indicating the total time it
-takes to reach at that point, but the total time is broken down into the
+takes to reach that point, but the total time is broken down into the
multiple stages an idea has to pass through to be shipped.
-Cycle Analytics is that it is tightly coupled with the [GitLab flow] and
+Cycle Analytics is tightly coupled with the [GitLab flow] and
calculates a separate median for each stage.
## Overview
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
index 2a890acde4d..73fa83d72a8 100644
--- a/doc/user/project/integrations/kubernetes.md
+++ b/doc/user/project/integrations/kubernetes.md
@@ -48,8 +48,12 @@ GitLab CI build environment:
- `KUBE_URL` - equal to the API URL
- `KUBE_TOKEN`
-- `KUBE_NAMESPACE`
-- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path to a file containing PEM data.
+- `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified.
+ The default value is `<project_name>-<project_id>`. You can overwrite it to
+ use different one if needed, otherwise the `KUBE_NAMESPACE` variable will
+ receive the default value.
+- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path
+ to a file containing PEM data.
- `KUBE_CA_PEM` (deprecated)- only if a custom CA bundle was specified. Raw PEM data.
## Web terminals
@@ -60,7 +64,7 @@ to use terminals. Support is currently limited to the first container in the
first pod of your environment.
When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals)
-support to your environments. This is based on the `exec` functionality found in
+support to your [environments](../../../ci/environments.md). This is based on the `exec` functionality found in
Docker and Kubernetes, so you get a new shell session within your existing
containers. To use this integration, you should deploy to Kubernetes using
the deployment variables above, ensuring any pods you create are labelled with
diff --git a/doc/user/project/integrations/microsoft_teams.md b/doc/user/project/integrations/microsoft_teams.md
index fbf9c1de443..eaad2d5138a 100644
--- a/doc/user/project/integrations/microsoft_teams.md
+++ b/doc/user/project/integrations/microsoft_teams.md
@@ -1,8 +1,8 @@
-# Microsoft Teams Service
+# Microsoft Teams service
## On Microsoft Teams
-To enable Microsoft Teams integration you must create an incoming webhook integration on Microsoft Teams by following the steps described in this [document](https://msdn.microsoft.com/en-us/microsoft-teams/connectors)
+To enable Microsoft Teams integration you must create an incoming webhook integration on Microsoft Teams by following the steps described in this [document](https://msdn.microsoft.com/en-us/microsoft-teams/connectors).
## On GitLab
@@ -30,4 +30,4 @@ At the end fill in your Microsoft Teams details:
After you are all done, click **Save changes** for the changes to take effect.
-![Microsoft Teams configuration](img/microsoft_teams_configuration.png) \ No newline at end of file
+![Microsoft Teams configuration](img/microsoft_teams_configuration.png)
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 25400633de5..96c91093d7d 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -47,6 +47,7 @@ Click on the service links to see further configuration instructions and details
| [Kubernetes](kubernetes.md) | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
+| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors |
| Pipelines emails | Email the pipeline status to a list of recipients |
| [Slack Notifications](slack.md) | Receive event notifications in Slack |
| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index c759b7aaa4a..954454f7e7a 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -34,7 +34,7 @@ Keep track of the progress during a code review with resolving comments.
Resolving comments prevents you from forgetting to address feedback and lets
you hide discussions that are no longer relevant.
-[Read more about resolving discussion comments in merge requests reviews.](merge_request_discussion_resolution.md)
+[Read more about resolving discussion comments in merge requests reviews.](../../discussions/index.md)
## Resolve conflicts
diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
index 230e957f045..200965875a1 100644
--- a/doc/user/project/merge_requests/merge_request_discussion_resolution.md
+++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
@@ -1,106 +1 @@
-# Merge Request discussion resolution
-
-> [Introduced][ce-5022] in GitLab 8.11.
-
-Discussion resolution helps keep track of progress during code review.
-Resolving comments prevents you from forgetting to address feedback and lets you
-hide discussions that are no longer relevant.
-
-!["A discussion between two people on a piece of code"][discussion-view]
-
-Comments and discussions can be resolved by anyone with at least Developer
-access to the project, as well as by the author of the merge request.
-
-## Marking a comment or discussion as resolved
-
-You can mark a discussion as resolved by clicking the "Resolve discussion"
-button at the bottom of the discussion.
-
-!["Resolve discussion" button][resolve-discussion-button]
-
-Alternatively, you can mark each comment as resolved individually.
-
-!["Resolve comment" button][resolve-comment-button]
-
-## Jumping between unresolved discussions
-
-When a merge request has a large number of comments it can be difficult to track
-what remains unresolved. You can jump between unresolved discussions with the
-Jump button next to the Reply field on a discussion.
-
-You can also jump to the first unresolved discussion from the button next to the
-resolved discussions tracker.
-
-!["3/4 discussions resolved"][discussions-resolved]
-
-## Only allow merge requests to be merged if all discussions are resolved
-
-> [Introduced][ce-7125] in GitLab 8.14.
-
-You can prevent merge requests from being merged until all discussions are
-resolved.
-
-Navigate to your project's settings page, select the
-**Only allow merge requests to be merged if all discussions are resolved** check
-box and hit **Save** for the changes to take effect.
-
-![Only allow merge if all the discussions are resolved settings](img/only_allow_merge_if_all_discussions_are_resolved.png)
-
-From now on, you will not be able to merge from the UI until all discussions
-are resolved.
-
-![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png)
-
-## Move all unresolved discussions in a merge request to an issue
-
-> [Introduced][ce-8266]
-
-To continue all open discussions in a merge request, click the button **Resolve
-all discussions in new issue**
-
-![Open new issue for all unresolved discussions](img/btn_new_issue_for_all_discussions.png)
-
-Alternatively, when your project only accepts merge requests when all discussions
-are resolved, there will be an **open an issue to resolve them later** link in
-the merge request-widget.
-
-![Link in merge request widget](img/resolve_discussion_open_issue.png)
-
-This will prepare an issue with content referring to the merge request and
-discussions.
-
-![Issue mentioning discussions in a merge request](img/preview_issue_for_discussions.png)
-
-Hitting **Submit issue** will cause all discussions to be marked as resolved and
-add a note referring to the newly created issue.
-
-![Mark discussions as resolved notice](img/resolve_discussion_issue_notice.png)
-
-You can now proceed to merge the merge request from the UI.
-
-## Moving a single discussion to a new issue
-
-> [Introduced][ce-8266]
-
-To create a new issue for a single discussion, you can use the **Resolve this
-discussion in a new issue** button.
-
-![Create issue for discussion](img/new_issue_for_discussion.png)
-
-This will direct you to a new issue prefilled with the content of the
-discussion, similar to the issues created for delegating multiple
-discussions at once.
-
-![New issue for a single discussion](img/preview_issue_for_discussion.png)
-
-Saving the issue will mark the discussion as resolved and add a note
-to the discussion referencing the new issue.
-
-[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
-[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
-[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
-[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
-[resolve-discussion-button]: img/resolve_discussion_button.png
-[resolve-comment-button]: img/resolve_comment_button.png
-[discussion-view]: img/discussion_view.png
-[discussions-resolved]: img/discussions_resolved.png
+This document was moved to [another location](../../discussions/index.md).
diff --git a/doc/user/project/milestones/img/milestone_create.png b/doc/user/project/milestones/img/milestone_create.png
new file mode 100644
index 00000000000..beb2caa897f
--- /dev/null
+++ b/doc/user/project/milestones/img/milestone_create.png
Binary files differ
diff --git a/doc/user/project/milestones/img/milestone_group_create.png b/doc/user/project/milestones/img/milestone_group_create.png
new file mode 100644
index 00000000000..7aaa7c56c15
--- /dev/null
+++ b/doc/user/project/milestones/img/milestone_group_create.png
Binary files differ
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
new file mode 100644
index 00000000000..a43a42a8fe8
--- /dev/null
+++ b/doc/user/project/milestones/index.md
@@ -0,0 +1,46 @@
+# Milestones
+
+Milestones allow you to organize issues and merge requests into a cohesive group,
+optionally setting a due date. A common use is keeping track of an upcoming
+software version. Milestones can be created per-project or per-group.
+
+## Creating a project milestone
+
+>**Note:**
+You need [Master permissions](../../permissions.md) in order to create a milestone.
+
+You can find the milestones page under your project's **Issues ➔ Milestones**.
+To create a new milestone, simply click the **New milestone** button when in the
+milestones page. A milestone can have a title, a description and start/due dates.
+Once you fill in all the details, hit the **Create milestone** button.
+
+![Creating a milestone](img/milestone_create.png)
+
+## Creating a group milestone
+
+>**Note:**
+You need [Master permissions](../../permissions.md) in order to create a milestone.
+
+You can create a milestone for several projects in the same group simultaneously.
+On the group's **Issues ➔ Milestones** page, you will be able to see the status
+of that milestone across all of the selected projects. To create a new milestone
+for selected projects in the group, click the **New milestone** button. The
+form is the same as when creating a milestone for a specific project with the
+addition of the selection of the projects you want to inherit this milestone.
+
+![Creating a group milestone](img/milestone_group_create.png)
+
+## Special milestone filters
+
+In addition to the milestones that exist in the project or group, there are some
+special options available when filtering by milestone:
+
+* **No Milestone** - only show issues or merge requests without a milestone.
+* **Upcoming** - show issues or merge request that belong to the next open
+ milestone with a due date, by project. (For example: if project A has
+ milestone v1 due in three days, and project B has milestone v2 due in a week,
+ then this will show issues or merge requests from milestone v1 in project A
+ and milestone v2 in project B.)
+* **Started** - show issues or merge requests from any milestone with a start
+ date less than today. Note that this can return results from several
+ milestones in the same project.
diff --git a/doc/user/search/img/search_history.gif b/doc/user/search/img/search_history.gif
new file mode 100644
index 00000000000..4cfa48ee0ab
--- /dev/null
+++ b/doc/user/search/img/search_history.gif
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 9d1ca1adcb2..45f443819ec 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -40,6 +40,12 @@ The same process is valid for merge requests. Navigate to your project's **Merge
and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
milestone, and label.
+## Search History
+
+You can view recent searches by clicking on the little arrow-clock icon, which is to the left of the search input. Click the search entry to run that search again. This feature is available for issues and merge requests. Searches are stored locally in your browser.
+
+![search history](img/search_history.gif)
+
### Shortcut
You'll also find a shortcut on the search field on the top-right of the project's dashboard to
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index a1852650cfb..604c7d5cefb 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -27,12 +27,12 @@
- [Time tracking](time_tracking.md)
- [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md)
-- [Milestones](milestones.md)
+- [Milestones](../user/project/milestones/index.md)
- [Merge Requests](../user/project/merge_requests/index.md)
- [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md)
- [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md)
- [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md)
- - [Resolve discussion comments in merge requests reviews](../user/project/merge_requests/merge_request_discussion_resolution.md)
+ - [Resolve discussion comments in merge requests reviews](../user/discussions/index.md)
- [Resolve merge conflicts in the UI](../user/project/merge_requests/resolve_conflicts.md)
- [Revert changes in the UI](../user/project/merge_requests/revert_changes.md)
- [Merge requests versions](../user/project/merge_requests/versions.md)
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index d12c0c6d0c4..1b172b21f3d 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -313,5 +313,4 @@ Merging only when needed prevents creating merge commits in your feature branch
### References
-- [Sketch file](https://www.dropbox.com/s/58dvsj5votbwrzv/git_flows.sketch?dl=0) with vectors of images in this article
- [Git Flow by Vincent Driessen](http://nvie.com/posts/a-successful-git-branching-model/)
diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md
index 882747e14e9..1cb3c940f00 100644
--- a/doc/workflow/groups.md
+++ b/doc/workflow/groups.md
@@ -1,6 +1,6 @@
# GitLab Groups
-GitLab groups allow you to group projects into directories and give users to several projects at once.
+GitLab groups allow you to group projects into directories and give users access to several projects at once.
When you create a new project in GitLab, the default namespace for the project is the personal namespace associated with your GitLab user.
In this document we will see how to create groups, put projects in groups and manage who can access the projects in a group.
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index aece4ab34ba..8ed1d98d05b 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -10,6 +10,11 @@ in your GitLab instance sitewide. This configuration is optional, users will
still be able to import their GitHub repositories with a
[personal access token][gh-token].
+>**Note:**
+Administrators of a GitLab instance (Community or Enterprise Edition) can also
+use the [GitHub rake task][gh-rake] to import projects from GitHub without the
+constrains of a Sidekiq worker.
+
- At its current state, GitHub importer can import:
- the repository description (GitLab 7.7+)
- the Git repository data (GitLab 7.7+)
@@ -112,5 +117,6 @@ You can also choose a different name for the project and a different namespace,
if you have the privileges to do so.
[gh-import]: ../../integration/github.md "GitHub integration"
+[gh-rake]: ../../administration/raketasks/github_import.md "GitHub rake task"
[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
diff --git a/doc/workflow/milestones.md b/doc/workflow/milestones.md
index 37afe553e55..69eb6b286b0 100644
--- a/doc/workflow/milestones.md
+++ b/doc/workflow/milestones.md
@@ -1,28 +1 @@
-# Milestones
-
-Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
-A common use is keeping track of an upcoming software version. Milestones are created per-project.
-
-![milestone form](milestones/form.png)
-
-## Groups and milestones
-
-You can create a milestone for several projects in the same group simultaneously.
-On the group's milestones page, you will be able to see the status of that milestone across all of the selected projects.
-
-![group milestone form](milestones/group_form.png)
-
-## Special milestone filters
-
-In addition to the milestones that exist in the project or group, there are some
-special options available when filtering by milestone:
-
-* **No Milestone** - only show issues or merge requests without a milestone.
-* **Upcoming** - show issues or merge request that belong to the next open
- milestone with a due date, by project. (For example: if project A has
- milestone v1 due in three days, and project B has milestone v2 due in a week,
- then this will show issues or merge requests from milestone v1 in project A
- and milestone v2 in project B.)
-* **Started** - show issues or merge requests from any milestone with a start
- date less than today. Note that this can return results from several
- milestones in the same project.
+This document was moved to [another location](../user/project/milestones/index.md).
diff --git a/doc/workflow/milestones/form.png b/doc/workflow/milestones/form.png
deleted file mode 100644
index c4731d88543..00000000000
--- a/doc/workflow/milestones/form.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/milestones/group_form.png b/doc/workflow/milestones/group_form.png
deleted file mode 100644
index dccdb019703..00000000000
--- a/doc/workflow/milestones/group_form.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index f94357abec9..c5b7488be69 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -75,3 +75,9 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>r</kbd> | Reply (quoting selected text) |
| <kbd>e</kbd> | Edit issue/merge request |
| <kbd>l</kbd> | Change label |
+
+## Wiki pages
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>e</kbd> | Edit wiki page|
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 4b0fba842e9..3d8d3ce8f13 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -111,7 +111,7 @@ There are four kinds of filters you can use on your Todos dashboard.
| Type | Filter by issue or merge request |
| Action | Filter by the action that triggered the Todo |
-You can also filter by more than one of these at the same time.
+You can also filter by more than one of these at the same time. The possible Actions are `Any Action`, `Assigned`, `Mentioned`, `Added`, `Pipelines`, and `Directly Addressed`, [as described above](#what-triggers-a-todo).
[ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817
[ce-7926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7926
diff --git a/features/group/members.feature b/features/group/members.feature
index 1f9514bac39..e539f6a1273 100644
--- a/features/group/members.feature
+++ b/features/group/members.feature
@@ -4,40 +4,6 @@ Feature: Group Members
And "John Doe" is owner of group "Owned"
And "John Doe" is guest of group "Guest"
- @javascript
- Scenario: I should add user to group "Owned"
- Given User "Mary Jane" exists
- When I visit group "Owned" members page
- And I select user "Mary Jane" from list with role "Reporter"
- Then I should see user "Mary Jane" in team list
-
- @javascript
- Scenario: Add user to group
- Given gitlab user "Mike"
- When I visit group "Owned" members page
- When I select "Mike" as "Reporter"
- Then I should see "Mike" in team list as "Reporter"
-
- @javascript
- Scenario: Ignore add user to group when is already Owner
- Given gitlab user "Mike"
- When I visit group "Owned" members page
- When I select "Mike" as "Reporter"
- Then I should see "Mike" in team list as "Owner"
-
- @javascript
- Scenario: Invite user to group
- When I visit group "Owned" members page
- When I select "sjobs@apple.com" as "Reporter"
- Then I should see "sjobs@apple.com" in team list as invited "Reporter"
-
- @javascript
- Scenario: Edit group member permissions
- Given "Mary Jane" is guest of group "Owned"
- And I visit group "Owned" members page
- When I change the "Mary Jane" role to "Developer"
- Then I should see "Mary Jane" as "Developer"
-
# Leave
@javascript
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index 27fa67c1843..4dee0cd23dc 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -177,9 +177,3 @@ Feature: Project Issues
And I should not see labels field
And I submit new issue "500 error on profile"
Then I should see issue "500 error on profile"
-
- @javascript
- Scenario: Another user adds a comment to issue I'm currently viewing
- Given I visit issue page "Release 0.4"
- And another user adds a comment with text "Yay!" to issue "Release 0.4"
- Then I should see a new comment with text "Yay!"
diff --git a/features/project/merge_requests/revert.feature b/features/project/merge_requests/revert.feature
index ec6666f227f..aaac5fd7209 100644
--- a/features/project/merge_requests/revert.feature
+++ b/features/project/merge_requests/revert.feature
@@ -25,7 +25,5 @@ Feature: Revert Merge Requests
@javascript
Scenario: I revert a merge request in a new merge request
Given I click on the revert button
- And I am on the Merge Request detail page
- And I click on the revert button
And I revert the changes in a new merge request
Then I should see the new merge request notice
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index 894c4a96bb8..d81bc9802bc 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -117,6 +117,8 @@ Feature: Project Source Browse Files
And I click on ".gitignore" file in repo
And I see the ".gitignore"
And I click on "Replace"
+ Then I should see a Fork/Cancel combo
+ And I click button "Fork"
Then I should see a notice about a new fork having been created
When I click on "Replace"
And I replace it with a text file
@@ -135,7 +137,7 @@ Feature: Project Source Browse Files
And I fill the commit message
And I click on "Commit changes"
Then I am on the new file page
- And I see a commit error message
+ And I see "Path can contain only..."
@javascript
Scenario: I can create file with a directory name
@@ -265,6 +267,8 @@ Feature: Project Source Browse Files
And I click on ".gitignore" file in repo
And I see the ".gitignore"
And I click on "Delete"
+ Then I should see a Fork/Cancel combo
+ And I click button "Fork"
Then I should see a notice about a new fork having been created
When I click on "Delete"
And I fill the commit message
diff --git a/features/project/team_management.feature b/features/project/team_management.feature
index 5888662fc3f..aed41924cd9 100644
--- a/features/project/team_management.feature
+++ b/features/project/team_management.feature
@@ -7,26 +7,6 @@ Feature: Project Team Management
And "Dmitriy" is "Shop" developer
And I visit project "Shop" team page
- Scenario: See all team members
- Then I should be able to see myself in team
- And I should see "Dmitriy" in team list
-
- @javascript
- Scenario: Add user to project
- When I select "Mike" as "Reporter"
- Then I should see "Mike" in team list as "Reporter"
-
- @javascript
- Scenario: Invite user to project
- When I select "sjobs@apple.com" as "Reporter"
- Then I should see "sjobs@apple.com" in team list as invited "Reporter"
-
- @javascript
- Scenario: Update user access
- Given I should see "Dmitriy" in team list as "Developer"
- And I change "Dmitriy" role to "Reporter"
- And I should see "Dmitriy" in team list as "Reporter"
-
Scenario: Cancel team member
Given I click cancel link for "Dmitriy"
Then I visit project "Shop" team page
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index adaf375453c..b04a7015d4e 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -4,71 +4,6 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
include SharedPaths
include SharedGroup
include SharedUser
- include Select2Helper
-
- step 'I select "Mike" as "Reporter"' do
- user = User.find_by(name: "Mike")
-
- page.within ".users-group-form" do
- select2(user.id, from: "#user_ids", multiple: true)
- select "Reporter", from: "access_level"
- end
-
- click_button "Add to group"
- end
-
- step 'I select "Mike" as "Master"' do
- user = User.find_by(name: "Mike")
-
- page.within ".users-group-form" do
- select2(user.id, from: "#user_ids", multiple: true)
- select "Master", from: "access_level"
- end
-
- click_button "Add to group"
- end
-
- step 'I should see "Mike" in team list as "Reporter"' do
- page.within '.content-list' do
- expect(page).to have_content('Mike')
- expect(page).to have_content('Reporter')
- end
- end
-
- step 'I should see "Mike" in team list as "Owner"' do
- page.within '.content-list' do
- expect(page).to have_content('Mike')
- expect(page).to have_content('Owner')
- end
- end
-
- step 'I select "sjobs@apple.com" as "Reporter"' do
- page.within ".users-group-form" do
- select2("sjobs@apple.com", from: "#user_ids", multiple: true)
- select "Reporter", from: "access_level"
- end
-
- click_button "Add to group"
- end
-
- step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
- page.within '.content-list' do
- expect(page).to have_content('sjobs@apple.com')
- expect(page).to have_content('Invited')
- expect(page).to have_content('Reporter')
- end
- end
-
- step 'I select user "Mary Jane" from list with role "Reporter"' do
- user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane")
-
- page.within ".users-group-form" do
- select2(user.id, from: "#user_ids", multiple: true)
- select "Reporter", from: "access_level"
- end
-
- click_button "Add to group"
- end
step 'I should see user "John Doe" in team list' do
expect(group_members_list).to have_content("John Doe")
@@ -87,7 +22,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
end
step 'I click on the "Remove User From Group" button for "John Doe"' do
- find(:css, 'li', text: "John Doe").find(:css, 'a.btn-remove').click
+ find(:css, '.project-members-page li', text: "John Doe").find(:css, 'a.btn-remove').click
# poltergeist always confirms popups.
end
@@ -97,7 +32,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
end
step 'I should not see the "Remove User From Group" button for "John Doe"' do
- expect(find(:css, 'li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove')
+ expect(find(:css, '.project-members-page li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove')
# poltergeist always confirms popups.
end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 97ffd4b4ea2..de737cdc823 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -178,11 +178,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
def select_using_dropdown(dropdown_type, selection, is_commit = false)
dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click
+ dropdown.find('.dropdown-menu', visible: true)
dropdown.fill_in("Filter by Git revision", with: selection)
if is_commit
dropdown.find('input[type="search"]').send_keys(:return)
else
find_link(selection, visible: true).click
end
+ dropdown.find('.dropdown-menu', visible: false)
end
end
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index a4cfc1fb8c8..dfd0bc13305 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -87,7 +87,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'I search "hand"' do
- fill_in 'emoji_search', with: 'hand'
+ fill_in 'emoji-menu-search', with: 'hand'
end
step 'I see search result for "hand"' do
@@ -101,7 +101,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'The search field is focused' do
- expect(page).to have_selector('#emoji_search')
- expect(page.evaluate_script('document.activeElement.id')).to eq('emoji_search')
+ expect(page).to have_selector('.js-emoji-menu-search')
+ expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
end
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index c0dc48f1bb2..637e6568267 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -345,17 +345,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
end
- step 'another user adds a comment with text "Yay!" to issue "Release 0.4"' do
- issue = Issue.find_by!(title: 'Release 0.4')
- create(:note_on_issue, noteable: issue, project: project, note: 'Yay!')
- end
-
- step 'I should see a new comment with text "Yay!"' do
- page.within '#notes' do
- expect(page).to have_content('Yay!')
- end
- end
-
def filter_issue(text)
fill_in 'issuable_search', with: text
end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 975c879149e..280d70925f7 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -66,12 +66,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps
expect(page).not_to have_link('Remove avatar')
end
- step 'I should see project "Shop" version' do
- page.within '.project-side' do
- expect(page).to have_content '6.7.0.pre'
- end
- end
-
step 'change project default branch' do
select 'fix', from: 'project_default_branch'
click_button 'Save changes'
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 5bd3c1a1246..b4741f06d1b 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -87,9 +87,9 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I fill the new branch name' do
first('button.js-target-branch', visible: true).click
- first('.create-new-branch', visible: true).click
- first('#new_branch_name', visible: true).set('new_branch_name')
- first('.js-new-branch-btn', visible: true).click
+ find('.create-new-branch', visible: true).click
+ find('#new_branch_name', visible: true).set('new_branch_name')
+ find('.js-new-branch-btn', visible: true).click
end
step 'I fill the new file name with an illegal name' do
@@ -284,7 +284,11 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I see "Unable to create directory"' do
- expect(page).to have_content('Directory already exists')
+ expect(page).to have_content('A directory with this name already exists')
+ end
+
+ step 'I see "Path can contain only..."' do
+ expect(page).to have_content('Path can contain only')
end
step 'I see a commit error message' do
@@ -373,7 +377,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I should see a Fork/Cancel combo' do
expect(page).to have_link 'Fork'
expect(page).to have_button 'Cancel'
- expect(page).to have_content 'You don\'t have permission to edit this file. Try forking this project to edit the file.'
end
step 'I should see a notice about a new fork having been created' do
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index 6986c7ede56..ff4c9deee2a 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -4,25 +4,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
include SharedPaths
include Select2Helper
- step 'I should be able to see myself in team' do
- expect(page).to have_content(@user.name)
- expect(page).to have_content(@user.username)
- end
-
- step 'I should see "Dmitriy" in team list' do
+ step 'I should not see "Dmitriy" in team list' do
user = User.find_by(name: "Dmitriy")
- expect(page).to have_content(user.name)
- expect(page).to have_content(user.username)
- end
-
- step 'I select "Mike" as "Reporter"' do
- user = User.find_by(name: "Mike")
-
- page.within ".users-project-form" do
- select2(user.id, from: "#user_ids", multiple: true)
- select "Reporter", from: "access_level"
- end
- click_button "Add to project"
+ expect(page).not_to have_content(user.name)
+ expect(page).not_to have_content(user.username)
end
step 'I should see "Mike" in team list as "Reporter"' do
@@ -34,60 +19,6 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
end
- step 'I select "sjobs@apple.com" as "Reporter"' do
- page.within ".users-project-form" do
- find('#user_ids', visible: false).set('sjobs@apple.com')
- select "Reporter", from: "access_level"
- end
- click_button "Add to project"
- end
-
- step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
- project_member = project.project_members.find_by(invite_email: 'sjobs@apple.com')
- page.within "#project_member_#{project_member.id}" do
- expect(page).to have_content('sjobs@apple.com')
- expect(page).to have_content('Invited')
- expect(page).to have_content('Reporter')
- end
- end
-
- step 'I should see "Dmitriy" in team list as "Developer"' do
- user = User.find_by(name: 'Dmitriy')
- project_member = project.project_members.find_by(user_id: user.id)
- page.within "#project_member_#{project_member.id}" do
- expect(page).to have_content('Dmitriy')
- expect(page).to have_content('Developer')
- end
- end
-
- step 'I change "Dmitriy" role to "Reporter"' do
- project = Project.find_by(name: "Shop")
- user = User.find_by(name: 'Dmitriy')
- project_member = project.project_members.find_by(user_id: user.id)
- page.within "#project_member_#{project_member.id}" do
- click_button project_member.human_access
-
- page.within '.dropdown-menu' do
- click_link 'Reporter'
- end
- end
- end
-
- step 'I should see "Dmitriy" in team list as "Reporter"' do
- user = User.find_by(name: 'Dmitriy')
- project_member = project.project_members.find_by(user_id: user.id)
- page.within "#project_member_#{project_member.id}" do
- expect(page).to have_content('Dmitriy')
- expect(page).to have_content('Reporter')
- end
- end
-
- step 'I should not see "Dmitriy" in team list' do
- user = User.find_by(name: "Dmitriy")
- expect(page).not_to have_content(user.name)
- expect(page).not_to have_content(user.username)
- end
-
step 'gitlab user "Mike"' do
create(:user, name: "Mike")
end
@@ -113,7 +44,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
project.team << [user, :reporter]
end
- step 'I click link "Import team from another project"' do
+ step 'I click link "Import team from another project"' do
page.within '.users-project-form' do
click_link "Import"
end
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 66b37fd2bcc..621b9dcecd9 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -62,7 +62,7 @@ module API
post ":id/repository/commits" do
authorize! :push_code, user_project
- attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch])
+ attrs = declared_params.merge(start_branch: declared_params[:branch], branch_name: declared_params[:branch])
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
@@ -140,7 +140,7 @@ module API
commit_params = {
commit: commit,
start_branch: params[:branch],
- target_branch: params[:branch]
+ branch_name: params[:branch]
}
result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9919762cd82..64ab6f01eb5 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -18,6 +18,12 @@ module API
expose :bio, :location, :skype, :linkedin, :twitter, :website_url, :organization
end
+ class UserActivity < Grape::Entity
+ expose :username
+ expose :last_activity_on
+ expose :last_activity_on, as: :last_activity_at # Back-compat
+ end
+
class Identity < Grape::Entity
expose :provider, :extern_uid
end
@@ -25,6 +31,7 @@ module API
class UserPublic < User
expose :last_sign_in_at
expose :confirmed_at
+ expose :last_activity_on
expose :email
expose :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 33fc970dc09..e6ea12c5ab7 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -5,7 +5,7 @@ module API
{
file_path: attrs[:file_path],
start_branch: attrs[:branch],
- target_branch: attrs[:branch],
+ branch_name: attrs[:branch],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
file_content_encoding: attrs[:encoding],
@@ -130,7 +130,7 @@ module API
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
- result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute
+ result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute
if result[:status] != :success
render_api_error!(result[:message], 400)
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 32bbf956d7f..09d105f6b4c 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -5,11 +5,16 @@ module API
before { authenticate! }
helpers do
- params :optional_params do
+ params :optional_params_ce do
optional :description, type: String, desc: 'The description of the group'
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the group'
optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
+ optional :share_with_group_lock, type: Boolean, desc: 'Prevent sharing a project with another group within this group'
+ end
+
+ params :optional_params do
+ use :optional_params_ce
end
params :statistics_params do
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 2135a787b11..718f936a1fc 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -53,12 +53,18 @@ module API
]
end
- def parse_allowed_environment_variables
- return if params[:env].blank?
+ def parse_env
+ return {} if params[:env].blank?
JSON.parse(params[:env])
-
rescue JSON::ParserError
+ {}
+ end
+
+ def log_user_activity(actor)
+ commands = Gitlab::GitAccess::DOWNLOAD_COMMANDS
+
+ ::Users::ActivityService.new(actor, 'Git SSH').execute if commands.include?(params[:action])
end
end
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 70d0d57204d..5b48ee8665f 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -11,14 +11,16 @@ module API
# Params:
# key_id - ssh key id for Git over SSH
# user_id - user id for Git over HTTP
+ # protocol - Git access protocol being used, e.g. HTTP or SSH
# project - project path with namespace
# action - git action (git-upload-pack or git-receive-pack)
- # ref - branch name
- # forced_push - forced_push
- # protocol - Git access protocol being used, e.g. HTTP or SSH
+ # changes - changes as "oldrev newrev ref", see Gitlab::ChangesList
post "/allowed" do
status 200
+ # Stores some Git-specific env thread-safely
+ Gitlab::Git::Env.set(parse_env)
+
actor =
if params[:key_id]
Key.find_by(id: params[:key_id])
@@ -30,22 +32,16 @@ module API
actor.update_last_used_at if actor.is_a?(Key)
- access =
- if wiki?
- Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
- else
- Gitlab::GitAccess.new(actor,
- project,
- protocol,
- authentication_abilities: ssh_authentication_abilities,
- env: parse_allowed_environment_variables)
- end
-
- access_status = access.check(params[:action], params[:changes])
+ access_checker = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
+ access_status = access_checker
+ .new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
+ .check(params[:action], params[:changes])
response = { status: access_status.status, message: access_status.message }
if access_status.status
+ log_user_activity(actor)
+
# Return the repository full path so that gitlab-shell has it when
# handling ssh commands
response[:repository_path] =
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 09053e615cb..244725bb292 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -30,14 +30,18 @@ module API
use :pagination
end
- params :issue_params do
+ params :issue_params_ce do
optional :description, type: String, desc: 'The description of an issue'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
optional :labels, type: String, desc: 'Comma-separated list of label names'
- optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY'
+ optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
end
+
+ params :issue_params do
+ use :issue_params_ce
+ end
end
resource :issues do
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index c8033664133..cb7aec47cf0 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -33,13 +33,17 @@ module API
end
end
- params :optional_params do
+ params :optional_params_ce do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
end
+
+ params :optional_params do
+ use :optional_params_ce
+ end
end
desc 'List merge requests' do
@@ -145,14 +149,24 @@ module API
success Entities::MergeRequest
end
params do
+ # CE
+ at_least_one_of_ce = [
+ :assignee_id,
+ :description,
+ :labels,
+ :milestone_id,
+ :remove_source_branch,
+ :state_event,
+ :target_branch,
+ :title
+ ]
optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen],
desc: 'Status of the merge request'
+
use :optional_params
- at_least_one_of :title, :target_branch, :description, :assignee_id,
- :milestone_id, :labels, :state_event,
- :remove_source_branch
+ at_least_one_of(*at_least_one_of_ce)
end
put ':id/merge_requests/:merge_request_iid' do
merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request)
@@ -173,6 +187,7 @@ module API
success Entities::MergeRequest
end
params do
+ # CE
optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
optional :should_remove_source_branch, type: Boolean,
desc: 'When true, the source branch will be deleted if possible'
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 53791166c33..87dfd1573a4 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -13,7 +13,7 @@ module API
optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
- optional :build_events, type: Boolean, desc: "Trigger hook on build events"
+ optional :job_events, type: Boolean, desc: "Trigger hook on job events"
optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
@@ -53,7 +53,10 @@ module API
use :project_hook_properties
end
post ":id/hooks" do
- hook = user_project.hooks.new(declared_params(include_missing: false))
+ hook_params = declared_params(include_missing: false)
+ hook_params[:build_events] = hook_params.delete(:job_events) { false }
+
+ hook = user_project.hooks.new(hook_params)
if hook.save
present hook, with: Entities::ProjectHook
@@ -74,7 +77,10 @@ module API
put ":id/hooks/:hook_id" do
hook = user_project.hooks.find(params.delete(:hook_id))
- if hook.update_attributes(declared_params(include_missing: false))
+ update_params = declared_params(include_missing: false)
+ update_params[:build_events] = update_params.delete(:job_events) if update_params[:job_events]
+
+ if hook.update_attributes(update_params)
present hook, with: Entities::ProjectHook
else
error!("Invalid url given", 422) if hook.errors[:url].present?
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 766fbea53e6..db4b31b55bc 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -6,12 +6,12 @@ module API
before { authenticate_non_get! }
helpers do
- params :optional_params do
+ params :optional_params_ce do
optional :description, type: String, desc: 'The description of the project'
optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
- optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled'
+ optional :jobs_enabled, type: Boolean, desc: 'Flag indication if jobs are enabled'
optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
@@ -22,6 +22,10 @@ module API
optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
end
+
+ params :optional_params do
+ use :optional_params_ce
+ end
end
resource :projects do
@@ -99,6 +103,7 @@ module API
end
post do
attrs = declared_params(include_missing: false)
+ attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.has_key?(:jobs_enabled)
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
@@ -198,17 +203,33 @@ module API
success Entities::Project
end
params do
+ # CE
+ at_least_one_of_ce =
+ [
+ :jobs_enabled,
+ :container_registry_enabled,
+ :default_branch,
+ :description,
+ :issues_enabled,
+ :lfs_enabled,
+ :merge_requests_enabled,
+ :name,
+ :only_allow_merge_if_all_discussions_are_resolved,
+ :only_allow_merge_if_pipeline_succeeds,
+ :path,
+ :public_builds,
+ :request_access_enabled,
+ :shared_runners_enabled,
+ :snippets_enabled,
+ :visibility,
+ :wiki_enabled,
+ ]
optional :name, type: String, desc: 'The name of the project'
optional :default_branch, type: String, desc: 'The default branch of the project'
optional :path, type: String, desc: 'The path of the repository'
+
use :optional_params
- at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
- :wiki_enabled, :builds_enabled, :snippets_enabled,
- :shared_runners_enabled, :container_registry_enabled,
- :lfs_enabled, :visibility, :public_builds,
- :request_access_enabled, :only_allow_merge_if_pipeline_succeeds,
- :only_allow_merge_if_all_discussions_are_resolved, :path,
- :default_branch
+ at_least_one_of(*at_least_one_of_ce)
end
put ':id' do
authorize_admin_project
@@ -216,6 +237,8 @@ module API
authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility].present?
+ attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.has_key?(:jobs_enabled)
+
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
if result[:status] == :success
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index c7f97ad2aab..d01c7f2703b 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -20,6 +20,55 @@ module API
success Entities::ApplicationSetting
end
params do
+ # CE
+ at_least_one_of_ce = [
+ :admin_notification_email,
+ :after_sign_out_path,
+ :after_sign_up_text,
+ :akismet_enabled,
+ :container_registry_token_expire_delay,
+ :default_artifacts_expire_in,
+ :default_branch_protection,
+ :default_group_visibility,
+ :default_project_visibility,
+ :default_projects_limit,
+ :default_snippet_visibility,
+ :disabled_oauth_sign_in_sources,
+ :domain_blacklist_enabled,
+ :domain_whitelist,
+ :email_author_in_body,
+ :enabled_git_access_protocol,
+ :gravatar_enabled,
+ :help_page_text,
+ :home_page_url,
+ :housekeeping_enabled,
+ :html_emails_enabled,
+ :import_sources,
+ :koding_enabled,
+ :max_artifacts_size,
+ :max_attachment_size,
+ :max_pages_size,
+ :metrics_enabled,
+ :plantuml_enabled,
+ :polling_interval_multiplier,
+ :recaptcha_enabled,
+ :repository_checks_enabled,
+ :repository_storage,
+ :require_two_factor_authentication,
+ :restricted_visibility_levels,
+ :send_user_confirmation_email,
+ :sentry_enabled,
+ :session_expire_delay,
+ :shared_runners_enabled,
+ :sidekiq_throttling_enabled,
+ :sign_in_text,
+ :signin_enabled,
+ :signup_enabled,
+ :terminal_max_session_time,
+ :user_default_external,
+ :user_oauth_applications,
+ :version_check_enabled
+ ]
optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master'
optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility'
optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
@@ -111,22 +160,8 @@ module API
end
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
- at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility,
- :default_group_visibility, :restricted_visibility_levels, :import_sources,
- :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
- :max_attachment_size, :session_expire_delay, :disabled_oauth_sign_in_sources,
- :user_oauth_applications, :user_default_external, :signup_enabled,
- :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
- :after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
- :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
- :shared_runners_enabled, :max_artifacts_size,
- :default_artifacts_expire_in, :max_pages_size,
- :container_registry_token_expire_delay,
- :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
- :akismet_enabled, :admin_notification_email, :sentry_enabled,
- :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
- :version_check_enabled, :email_author_in_body, :html_emails_enabled,
- :housekeeping_enabled, :terminal_max_session_time, :polling_interval_multiplier
+
+ at_least_one_of(*at_least_one_of_ce)
end
put "application/settings" do
attrs = declared_params(include_missing: false)
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 6f40f92240a..46f221f68fe 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -37,11 +37,16 @@ module API
success Entities::UserBasic
end
params do
+ # CE
optional :username, type: String, desc: 'Get a single user with a specific username'
+ optional :extern_uid, type: String, desc: 'Get a single user with a specific external authentication provider UID'
+ optional :provider, type: String, desc: 'The external provider'
optional :search, type: String, desc: 'Search for a username'
optional :active, type: Boolean, default: false, desc: 'Filters only active users'
optional :external, type: Boolean, default: false, desc: 'Filters only external users'
optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users'
+ all_or_none_of :extern_uid, :provider
+
use :pagination
end
get do
@@ -49,14 +54,17 @@ module API
render_api_error!("Not authorized.", 403)
end
- if params[:username].present?
- users = User.where(username: params[:username])
- else
- users = User.all
- users = users.active if params[:active]
- users = users.search(params[:search]) if params[:search].present?
- users = users.blocked if params[:blocked]
- users = users.external if params[:external] && current_user.admin?
+ authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
+
+ users = User.all
+ users = User.where(username: params[:username]) if params[:username]
+ users = users.active if params[:active]
+ users = users.search(params[:search]) if params[:search].present?
+ users = users.blocked if params[:blocked]
+
+ if current_user.admin?
+ users = users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid])) if params[:extern_uid] && params[:provider]
+ users = users.external if params[:external]
end
entity = current_user.admin? ? Entities::UserPublic : Entities::UserBasic
@@ -532,6 +540,21 @@ module API
email.destroy
current_user.update_secondary_emails!
end
+
+ desc 'Get a list of user activities'
+ params do
+ optional :from, type: DateTime, default: 6.months.ago, desc: 'Date string in the format YEAR-MONTH-DAY'
+ use :pagination
+ end
+ get "activities" do
+ authenticated_as_admin!
+
+ activities = User.
+ where(User.arel_table[:last_activity_on].gteq(params[:from])).
+ reorder(last_activity_on: :asc)
+
+ present paginate(activities), with: Entities::UserActivity
+ end
end
end
end
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
index 3414a2883e5..674de592f0a 100644
--- a/lib/api/v3/commits.rb
+++ b/lib/api/v3/commits.rb
@@ -53,7 +53,7 @@ module API
attrs = declared_params.dup
branch = attrs.delete(:branch_name)
- attrs.merge!(branch: branch, start_branch: branch, target_branch: branch)
+ attrs.merge!(start_branch: branch, branch_name: branch)
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
@@ -131,7 +131,7 @@ module API
commit_params = {
commit: commit,
start_branch: params[:branch],
- target_branch: params[:branch]
+ branch_name: params[:branch]
}
result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb
index 13542b0c71c..c76acc86504 100644
--- a/lib/api/v3/files.rb
+++ b/lib/api/v3/files.rb
@@ -6,7 +6,7 @@ module API
{
file_path: attrs[:file_path],
start_branch: attrs[:branch],
- target_branch: attrs[:branch],
+ branch_name: attrs[:branch],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
file_content_encoding: attrs[:encoding],
@@ -123,7 +123,7 @@ module API
file_params = declared_params(include_missing: false)
file_params[:branch] = file_params.delete(:branch_name)
- result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute
+ result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute
if result[:status] == :success
status(200)
diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb
index 6b78aa795b4..327ea9449a1 100644
--- a/lib/banzai/filter/issuable_state_filter.rb
+++ b/lib/banzai/filter/issuable_state_filter.rb
@@ -9,12 +9,14 @@ module Banzai
VISIBLE_STATES = %w(closed merged).freeze
def call
+ return doc unless context[:issuable_state_filter_enabled]
+
extractor = Banzai::IssuableExtractor.new(project, current_user)
issuables = extractor.extract([doc])
issuables.each do |node, issuable|
- if VISIBLE_STATES.include?(issuable.state)
- node.children.last.content += " [#{issuable.state}]"
+ if VISIBLE_STATES.include?(issuable.state) && node.inner_html == issuable.reference_link_text(project)
+ node.content += " (#{issuable.state})"
end
end
diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb
index b2537117558..5325819d828 100644
--- a/lib/banzai/filter/plantuml_filter.rb
+++ b/lib/banzai/filter/plantuml_filter.rb
@@ -7,14 +7,14 @@ module Banzai
#
class PlantumlFilter < HTML::Pipeline::Filter
def call
- return doc unless doc.at('pre.plantuml') && settings.plantuml_enabled
+ return doc unless doc.at('pre > code[lang="plantuml"]') && settings.plantuml_enabled
plantuml_setup
- doc.css('pre.plantuml').each do |el|
+ doc.css('pre > code[lang="plantuml"]').each do |node|
img_tag = Nokogiri::HTML::DocumentFragment.parse(
- Asciidoctor::PlantUml::Processor.plantuml_content(el.content, {}))
- el.replace img_tag
+ Asciidoctor::PlantUml::Processor.plantuml_content(node.content, {}))
+ node.parent.replace(img_tag)
end
doc
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index dabf71d6aeb..c2503fa2adc 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -136,7 +136,8 @@ module Banzai
nodes.each_with_object({}) do |node, hash|
if node.has_attribute?(attribute)
- hash[node] = objects_by_id[node.attr(attribute).to_i]
+ obj = objects_by_id[node.attr(attribute).to_i]
+ hash[node] = obj if obj
end
end
end
diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb
index 7d7dcce017e..84a28b33d7c 100644
--- a/lib/banzai/reference_parser/merge_request_parser.rb
+++ b/lib/banzai/reference_parser/merge_request_parser.rb
@@ -3,23 +3,42 @@ module Banzai
class MergeRequestParser < BaseParser
self.reference_type = :merge_request
+ def nodes_visible_to_user(user, nodes)
+ merge_requests = merge_requests_for_nodes(nodes)
+
+ nodes.select do |node|
+ merge_request = merge_requests[node]
+
+ merge_request && can?(user, :read_merge_request, merge_request.project)
+ end
+ end
+
+ def referenced_by(nodes)
+ merge_requests = merge_requests_for_nodes(nodes)
+
+ nodes.map { |node| merge_requests[node] }.compact.uniq
+ end
+
def merge_requests_for_nodes(nodes)
@merge_requests_for_nodes ||= grouped_objects_for_nodes(
nodes,
- MergeRequest.all,
+ MergeRequest.includes(
+ :author,
+ :assignee,
+ {
+ # These associations are primarily used for checking permissions.
+ # Eager loading these ensures we don't end up running dozens of
+ # queries in this process.
+ target_project: [
+ { namespace: :owner },
+ { group: [:owners, :group_members] },
+ :invited_groups,
+ :project_members
+ ]
+ }),
self.class.data_attribute
)
end
-
- def references_relation
- MergeRequest.includes(:author, :assignee, :target_project)
- end
-
- private
-
- def can_read_reference?(user, ref_project)
- can?(user, :read_merge_request, ref_project)
- end
end
end
end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index 74663556cbb..c7801cb5baf 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -1,7 +1,5 @@
module Banzai
module Renderer
- module_function
-
# Convert a Markdown String into an HTML-safe String of HTML
#
# Note that while the returned HTML will have been sanitized of dangerous
@@ -16,7 +14,7 @@ module Banzai
# context - Hash of context options passed to our HTML Pipeline
#
# Returns an HTML-safe String
- def render(text, context = {})
+ def self.render(text, context = {})
cache_key = context.delete(:cache_key)
cache_key = full_cache_key(cache_key, context[:pipeline])
@@ -35,24 +33,16 @@ module Banzai
# of HTML. This method is analogous to calling render(object.field), but it
# can cache the rendered HTML in the object, rather than Redis.
#
- # The context to use is learned from the passed-in object by calling
- # #banzai_render_context(field), and cannot be changed. Use #render, passing
- # it the field text, if a custom rendering is needed. The generated context
- # is returned along with the HTML.
- def render_field(object, field)
- html_field = object.markdown_cache_field_for(field)
-
- html = object.__send__(html_field)
- return html if html.present?
-
- html = cacheless_render_field(object, field)
- update_object(object, html_field, html) unless object.new_record? || object.destroyed?
+ # The context to use is managed by the object and cannot be changed.
+ # Use #render, passing it the field text, if a custom rendering is needed.
+ def self.render_field(object, field)
+ object.refresh_markdown_cache!(do_update: update_object?(object)) unless object.cached_html_up_to_date?(field)
- html
+ object.cached_html_for(field)
end
# Same as +render_field+, but without consulting or updating the cache field
- def cacheless_render_field(object, field, options = {})
+ def self.cacheless_render_field(object, field, options = {})
text = object.__send__(field)
context = object.banzai_render_context(field).merge(options)
@@ -82,7 +72,7 @@ module Banzai
# texts_and_contexts
# => [{ text: '### Hello',
# context: { cache_key: [note, :note] } }]
- def cache_collection_render(texts_and_contexts)
+ def self.cache_collection_render(texts_and_contexts)
items_collection = texts_and_contexts.each_with_index do |item, index|
context = item[:context]
cache_key = full_cache_multi_key(context.delete(:cache_key), context[:pipeline])
@@ -111,7 +101,7 @@ module Banzai
items_collection.map { |item| item[:rendered] }
end
- def render_result(text, context = {})
+ def self.render_result(text, context = {})
text = Pipeline[:pre_process].to_html(text, context) if text
Pipeline[context[:pipeline]].call(text, context)
@@ -130,7 +120,7 @@ module Banzai
# :user - User object
#
# Returns an HTML-safe String
- def post_process(html, context)
+ def self.post_process(html, context)
context = Pipeline[context[:pipeline]].transform_context(context)
pipeline = Pipeline[:post_process]
@@ -141,7 +131,7 @@ module Banzai
end.html_safe
end
- def cacheless_render(text, context = {})
+ def self.cacheless_render(text, context = {})
Gitlab::Metrics.measure(:banzai_cacheless_render) do
result = render_result(text, context)
@@ -154,7 +144,7 @@ module Banzai
end
end
- def full_cache_key(cache_key, pipeline_name)
+ def self.full_cache_key(cache_key, pipeline_name)
return unless cache_key
["banzai", *cache_key, pipeline_name || :full]
end
@@ -162,13 +152,14 @@ module Banzai
# To map Rails.cache.read_multi results we need to know the Rails.cache.expanded_key.
# Other option will be to generate stringified keys on our side and don't delegate to Rails.cache.expanded_key
# method.
- def full_cache_multi_key(cache_key, pipeline_name)
+ def self.full_cache_multi_key(cache_key, pipeline_name)
return unless cache_key
Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name))
end
- def update_object(object, html_field, html)
- object.update_column(html_field, html)
+ # GitLab EE needs to disable updates on GET requests in Geo
+ def self.update_object?(object)
+ true
end
end
end
diff --git a/lib/bitbucket/representation/base.rb b/lib/bitbucket/representation/base.rb
index 94adaacc9b5..800d5a075c6 100644
--- a/lib/bitbucket/representation/base.rb
+++ b/lib/bitbucket/representation/base.rb
@@ -1,6 +1,8 @@
module Bitbucket
module Representation
class Base
+ attr_reader :raw
+
def initialize(raw)
@raw = raw
end
@@ -8,10 +10,6 @@ module Bitbucket
def self.decorate(entries)
entries.map { |entry| new(entry)}
end
-
- private
-
- attr_reader :raw
end
end
end
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
index 1020452480a..b439b0ee29b 100644
--- a/lib/ci/ansi2html.rb
+++ b/lib/ci/ansi2html.rb
@@ -172,7 +172,7 @@ module Ci
close_open_tags()
OpenStruct.new(
- html: @out,
+ html: @out.force_encoding(Encoding.default_external),
state: state,
append: append,
truncated: truncated,
diff --git a/lib/container_registry/path.rb b/lib/container_registry/path.rb
index a4b5f2aba6c..61849a40383 100644
--- a/lib/container_registry/path.rb
+++ b/lib/container_registry/path.rb
@@ -15,7 +15,7 @@ module ContainerRegistry
LEVELS_SUPPORTED = 3
def initialize(path)
- @path = path
+ @path = path.to_s.downcase
end
def valid?
@@ -25,7 +25,7 @@ module ContainerRegistry
end
def components
- @components ||= @path.to_s.split('/')
+ @components ||= @path.split('/')
end
def nodes
@@ -48,7 +48,7 @@ module ContainerRegistry
end
def root_repository?
- @path == repository_project.full_path
+ @path == project_path
end
def repository_project
@@ -60,7 +60,13 @@ module ContainerRegistry
def repository_name
return unless has_project?
- @path.remove(%r(^#{Regexp.escape(repository_project.full_path)}/?))
+ @path.remove(%r(^#{Regexp.escape(project_path)}/?))
+ end
+
+ def project_path
+ return unless has_project?
+
+ repository_project.full_path.downcase
end
def to_s
diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb
index d00e6191e7e..728deea224f 100644
--- a/lib/container_registry/tag.rb
+++ b/lib/container_registry/tag.rb
@@ -29,6 +29,10 @@ module ContainerRegistry
"#{repository.path}:#{name}"
end
+ def location
+ "#{repository.location}:#{name}"
+ end
+
def [](key)
return unless manifest
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index f4efa20374a..5a6d9ae99a0 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -149,7 +149,7 @@ module Gitlab
description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author)
description += pull_request.description
- merge_request = project.merge_requests.create(
+ merge_request = project.merge_requests.create!(
iid: pull_request.iid,
title: pull_request.title,
description: description,
@@ -168,7 +168,7 @@ module Gitlab
import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
rescue StandardError => e
- errors << { type: :pull_request, iid: pull_request.iid, errors: e.message }
+ errors << { type: :pull_request, iid: pull_request.iid, errors: e.message, trace: e.backtrace.join("\n"), raw_response: pull_request.raw }
end
end
end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index eb2f2e144fd..8793b20aa35 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -5,7 +5,7 @@ module Gitlab
attr_reader :user_access, :project, :skip_authorization, :protocol
def initialize(
- change, user_access:, project:, env: {}, skip_authorization: false,
+ change, user_access:, project:, skip_authorization: false,
protocol:
)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@@ -13,7 +13,6 @@ module Gitlab
@tag_name = Gitlab::Git.tag_name(@ref)
@user_access = user_access
@project = project
- @env = env
@skip_authorization = skip_authorization
@protocol = protocol
end
@@ -97,7 +96,7 @@ module Gitlab
end
def forced_push?
- Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env)
+ Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev)
end
def update?
diff --git a/lib/gitlab/checks/force_push.rb b/lib/gitlab/checks/force_push.rb
index de0c9049ebf..1e73f89158d 100644
--- a/lib/gitlab/checks/force_push.rb
+++ b/lib/gitlab/checks/force_push.rb
@@ -1,20 +1,16 @@
module Gitlab
module Checks
class ForcePush
- def self.force_push?(project, oldrev, newrev, env: {})
+ def self.force_push?(project, oldrev, newrev)
return false if project.empty_repo?
# Created or deleted branch
if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
false
else
- missed_ref, exit_status = Gitlab::Git::RevList.new(oldrev, newrev, project: project, env: env).execute
-
- if exit_status == 0
- missed_ref.present?
- else
- raise "Got a non-zero exit code while calling out to `git rev-list` in the force-push check."
- end
+ Gitlab::Git::RevList.new(
+ path_to_repo: project.repository.path_to_repo,
+ oldrev: oldrev, newrev: newrev).missed_ref.present?
end
end
end
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index 2af94e2c60e..fa462cbe095 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -4,7 +4,7 @@ module Gitlab
# This was inspired from: http://stackoverflow.com/a/10219411/1520132
class Stream
BUFFER_SIZE = 4096
- LIMIT_SIZE = 50.kilobytes
+ LIMIT_SIZE = 500.kilobytes
attr_reader :stream
@@ -14,6 +14,7 @@ module Gitlab
def initialize
@stream = yield
+ @stream&.binmode
end
def valid?
@@ -25,11 +26,10 @@ module Gitlab
end
def limit(last_bytes = LIMIT_SIZE)
- stream_size = size
- if stream_size < last_bytes
- last_bytes = stream_size
+ if last_bytes < size
+ stream.seek(-last_bytes, IO::SEEK_END)
+ stream.readline
end
- stream.seek(-last_bytes, IO::SEEK_END)
end
def append(data, offset)
@@ -52,7 +52,7 @@ module Gitlab
read_last_lines(last_lines)
else
stream.read
- end
+ end.force_encoding(Encoding.default_external)
end
def html_with_state(state = nil)
@@ -61,8 +61,8 @@ module Gitlab
def html(last_lines: nil)
text = raw(last_lines: last_lines)
- stream = StringIO.new(text)
- ::Ci::Ansi2html.convert(stream).html
+ buffer = StringIO.new(text)
+ ::Ci::Ansi2html.convert(buffer).html
end
def extract_coverage(regex)
@@ -76,11 +76,14 @@ module Gitlab
stream.each_line do |line|
matches = line.scan(regex)
next unless matches.is_a?(Array)
+ next if matches.empty?
match = matches.flatten.last
coverage = match.gsub(/\d+(\.\d+)?/).first
- return coverage.to_f if coverage.present?
+ return coverage if coverage.present?
end
+
+ nil
rescue
# if bad regex or something goes wrong we dont want to interrupt transition
# so we just silentrly ignore error for now
@@ -111,7 +114,6 @@ module Gitlab
end
chunks.join.lines.last(last_lines).join
- .force_encoding(Encoding.default_external)
end
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 63b8d0d3b9d..d0bd1299671 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -57,16 +57,16 @@ module Gitlab
postgresql? ? "RANDOM()" : "RAND()"
end
- def true_value
- if Gitlab::Database.postgresql?
+ def self.true_value
+ if postgresql?
"'t'"
else
1
end
end
- def false_value
- if Gitlab::Database.postgresql?
+ def self.false_value
+ if postgresql?
"'f'"
else
0
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 525aa920328..6dabbe0264c 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -89,7 +89,8 @@ module Gitlab
ADD CONSTRAINT #{key_name}
FOREIGN KEY (#{column})
REFERENCES #{target} (id)
- ON DELETE #{on_delete} NOT VALID;
+ #{on_delete ? "ON DELETE #{on_delete}" : ''}
+ NOT VALID;
EOF
# Validate the existing constraint. This can potentially take a very
@@ -114,6 +115,14 @@ module Gitlab
execute('SET statement_timeout TO 0') if Database.postgresql?
end
+ def true_value
+ Database.true_value
+ end
+
+ def false_value
+ Database.false_value
+ end
+
# Updates the value of a column in batches.
#
# This method updates the table in batches of 5% of the total row count.
@@ -250,6 +259,245 @@ module Gitlab
raise error
end
end
+
+ # Renames a column without requiring downtime.
+ #
+ # Concurrent renames work by using database triggers to ensure both the
+ # old and new column are in sync. However, this method will _not_ remove
+ # the triggers or the old column automatically; this needs to be done
+ # manually in a post-deployment migration. This can be done using the
+ # method `cleanup_concurrent_column_rename`.
+ #
+ # 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.
+ def rename_column_concurrently(table, old, new, type: nil)
+ if transaction_open?
+ raise 'rename_column_concurrently can not be run inside a transaction'
+ end
+
+ trigger_name = rename_trigger_name(table, old, new)
+ quoted_table = quote_table_name(table)
+ quoted_old = quote_column_name(old)
+ quoted_new = quote_column_name(new)
+
+ if Database.postgresql?
+ install_rename_triggers_for_postgresql(trigger_name, quoted_table,
+ quoted_old, quoted_new)
+ else
+ install_rename_triggers_for_mysql(trigger_name, quoted_table,
+ quoted_old, quoted_new)
+ end
+
+ old_col = column_for(table, old)
+ new_type = type || old_col.type
+
+ add_column(table, new, new_type,
+ limit: old_col.limit,
+ default: old_col.default,
+ null: old_col.null,
+ precision: old_col.precision,
+ scale: old_col.scale)
+
+ update_column_in_batches(table, new, Arel::Table.new(table)[old])
+
+ copy_indexes(table, old, new)
+ copy_foreign_keys(table, old, new)
+ end
+
+ # Changes the type of a column concurrently.
+ #
+ # table - The table containing the column.
+ # column - The name of the column to change.
+ # new_type - The new column type.
+ def change_column_type_concurrently(table, column, new_type)
+ temp_column = "#{column}_for_type_change"
+
+ rename_column_concurrently(table, column, temp_column, type: new_type)
+ end
+
+ # Performs cleanup of a concurrent type change.
+ #
+ # table - The table containing the column.
+ # column - The name of the column to change.
+ # new_type - The new column type.
+ def cleanup_concurrent_column_type_change(table, column)
+ temp_column = "#{column}_for_type_change"
+
+ transaction do
+ # This has to be performed in a transaction as otherwise we might have
+ # inconsistent data.
+ cleanup_concurrent_column_rename(table, column, temp_column)
+ rename_column(table, temp_column, column)
+ end
+ end
+
+ # Cleans up a concurrent column name.
+ #
+ # This method takes care of removing previously installed triggers as well
+ # as removing the old column.
+ #
+ # table - The name of the database table.
+ # old - The name of the old column.
+ # new - The name of the new column.
+ def cleanup_concurrent_column_rename(table, old, new)
+ trigger_name = rename_trigger_name(table, old, new)
+
+ if Database.postgresql?
+ remove_rename_triggers_for_postgresql(table, trigger_name)
+ else
+ remove_rename_triggers_for_mysql(trigger_name)
+ end
+
+ remove_column(table, old)
+ end
+
+ # Performs a concurrent column rename when using PostgreSQL.
+ def install_rename_triggers_for_postgresql(trigger, table, old, new)
+ execute <<-EOF.strip_heredoc
+ CREATE OR REPLACE FUNCTION #{trigger}()
+ RETURNS trigger AS
+ $BODY$
+ BEGIN
+ NEW.#{new} := NEW.#{old};
+ RETURN NEW;
+ END;
+ $BODY$
+ LANGUAGE 'plpgsql'
+ VOLATILE
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ CREATE TRIGGER #{trigger}
+ BEFORE INSERT OR UPDATE
+ ON #{table}
+ FOR EACH ROW
+ EXECUTE PROCEDURE #{trigger}()
+ EOF
+ end
+
+ # Installs the triggers necessary to perform a concurrent column rename on
+ # MySQL.
+ def install_rename_triggers_for_mysql(trigger, table, old, new)
+ execute <<-EOF.strip_heredoc
+ CREATE TRIGGER #{trigger}_insert
+ BEFORE INSERT
+ ON #{table}
+ FOR EACH ROW
+ SET NEW.#{new} = NEW.#{old}
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ CREATE TRIGGER #{trigger}_update
+ BEFORE UPDATE
+ ON #{table}
+ FOR EACH ROW
+ SET NEW.#{new} = NEW.#{old}
+ EOF
+ end
+
+ # Removes the triggers used for renaming a PostgreSQL column concurrently.
+ def remove_rename_triggers_for_postgresql(table, trigger)
+ execute("DROP TRIGGER #{trigger} ON #{table}")
+ execute("DROP FUNCTION #{trigger}()")
+ end
+
+ # Removes the triggers used for renaming a MySQL column concurrently.
+ def remove_rename_triggers_for_mysql(trigger)
+ execute("DROP TRIGGER #{trigger}_insert")
+ execute("DROP TRIGGER #{trigger}_update")
+ end
+
+ # Returns the (base) name to use for triggers when renaming columns.
+ def rename_trigger_name(table, old, new)
+ 'trigger_' + Digest::SHA256.hexdigest("#{table}_#{old}_#{new}").first(12)
+ end
+
+ # Returns an Array containing the indexes for the given column
+ def indexes_for(table, column)
+ column = column.to_s
+
+ indexes(table).select { |index| index.columns.include?(column) }
+ end
+
+ # Returns an Array containing the foreign keys for the given column.
+ def foreign_keys_for(table, column)
+ column = column.to_s
+
+ foreign_keys(table).select { |fk| fk.column == column }
+ end
+
+ # Copies all indexes for the old column to a new column.
+ #
+ # table - The table containing the columns and indexes.
+ # old - The old column.
+ # new - The new column.
+ def copy_indexes(table, old, new)
+ old = old.to_s
+ new = new.to_s
+
+ indexes_for(table, old).each do |index|
+ new_columns = index.columns.map do |column|
+ column == old ? new : column
+ end
+
+ # This is necessary as we can't properly rename indexes such as
+ # "ci_taggings_idx".
+ unless index.name.include?(old)
+ raise "The index #{index.name} can not be copied as it does not "\
+ "mention the old column. You have to rename this index manually first."
+ end
+
+ name = index.name.gsub(old, new)
+
+ options = {
+ unique: index.unique,
+ name: name,
+ length: index.lengths,
+ order: index.orders
+ }
+
+ # These options are not supported by MySQL, so we only add them if
+ # they were previously set.
+ options[:using] = index.using if index.using
+ options[:where] = index.where if index.where
+
+ unless index.opclasses.blank?
+ opclasses = index.opclasses.dup
+
+ # Copy the operator classes for the old column (if any) to the new
+ # column.
+ opclasses[new] = opclasses.delete(old) if opclasses[old]
+
+ options[:opclasses] = opclasses
+ end
+
+ add_concurrent_index(table, new_columns, options)
+ end
+ end
+
+ # Copies all foreign keys for the old column to the new column.
+ #
+ # table - The table containing the columns and indexes.
+ # old - The old column.
+ # new - The new column.
+ def copy_foreign_keys(table, old, new)
+ foreign_keys_for(table, old).each do |fk|
+ add_concurrent_foreign_key(fk.from_table,
+ fk.to_table,
+ column: new,
+ on_delete: fk.on_delete)
+ end
+ end
+
+ # Returns the column for the given table and column name.
+ def column_for(table, name)
+ name = name.to_s
+
+ columns(table).find { |column| column.name == name }
+ end
end
end
end
diff --git a/lib/gitlab/database/multi_threaded_migration.rb b/lib/gitlab/database/multi_threaded_migration.rb
new file mode 100644
index 00000000000..7ae5a4c17c8
--- /dev/null
+++ b/lib/gitlab/database/multi_threaded_migration.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module Database
+ module MultiThreadedMigration
+ MULTI_THREAD_AR_CONNECTION = :thread_local_ar_connection
+
+ # This overwrites the default connection method so that every thread can
+ # use a thread-local connection, while still supporting all of Rails'
+ # migration methods.
+ def connection
+ Thread.current[MULTI_THREAD_AR_CONNECTION] ||
+ ActiveRecord::Base.connection
+ end
+
+ # Starts a thread-pool for N threads, along with N threads each using a
+ # single connection. The provided block is yielded from inside each
+ # thread.
+ #
+ # Example:
+ #
+ # with_multiple_threads(4) do
+ # execute('SELECT ...')
+ # end
+ #
+ # thread_count - The number of threads to start.
+ #
+ # join - When set to true this method will join the threads, blocking the
+ # caller until all threads have finished running.
+ #
+ # Returns an Array containing the started threads.
+ def with_multiple_threads(thread_count, join: true)
+ pool = Gitlab::Database.create_connection_pool(thread_count)
+
+ threads = Array.new(thread_count) do
+ Thread.new do
+ pool.with_connection do |connection|
+ begin
+ Thread.current[MULTI_THREAD_AR_CONNECTION] = connection
+ yield
+ ensure
+ Thread.current[MULTI_THREAD_AR_CONNECTION] = nil
+ end
+ end
+ end
+ end
+
+ threads.each(&:join) if join
+
+ threads
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb
index 8406ca4269c..7948782aecc 100644
--- a/lib/gitlab/diff/diff_refs.rb
+++ b/lib/gitlab/diff/diff_refs.rb
@@ -18,6 +18,12 @@ module Gitlab
head_sha == other.head_sha
end
+ alias_method :eql?, :==
+
+ def hash
+ [base_sha, start_sha, head_sha].hash
+ end
+
# There is only one case in which we will have `start_sha` and `head_sha`,
# but not `base_sha`, which is when a diff is generated between an
# orphaned branch and another branch, which means there _is_ no base, but
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
index 0e22f2189ee..c66b0435f3a 100644
--- a/lib/gitlab/email/handler/create_note_handler.rb
+++ b/lib/gitlab/email/handler/create_note_handler.rb
@@ -7,6 +7,8 @@ module Gitlab
class CreateNoteHandler < BaseHandler
include ReplyProcessing
+ delegate :project, to: :sent_notification, allow_nil: true
+
def can_handle?
mail_key =~ /\A\w+\z/
end
@@ -32,10 +34,6 @@ module Gitlab
sent_notification.recipient
end
- def project
- sent_notification.project
- end
-
def sent_notification
@sent_notification ||= SentNotification.for(mail_key)
end
diff --git a/lib/gitlab/email/handler/unsubscribe_handler.rb b/lib/gitlab/email/handler/unsubscribe_handler.rb
index 97d7a8d65ff..df491f060bf 100644
--- a/lib/gitlab/email/handler/unsubscribe_handler.rb
+++ b/lib/gitlab/email/handler/unsubscribe_handler.rb
@@ -4,6 +4,8 @@ module Gitlab
module Email
module Handler
class UnsubscribeHandler < BaseHandler
+ delegate :project, to: :sent_notification, allow_nil: true
+
def can_handle?
mail_key =~ /\A\w+#{Regexp.escape(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX)}\z/
end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index ec0529b5a4b..bb4fdd1f1f4 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -32,6 +32,10 @@ module Gitlab
raise UnknownIncomingEmail unless handler
+ Gitlab::Metrics.add_event(:receive_email,
+ project: handler.try(:project),
+ handler: handler.class.name)
+
handler.execute
end
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
index 11167632e07..270d67dd50c 100644
--- a/lib/gitlab/etag_caching/middleware.rb
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -1,40 +1,12 @@
module Gitlab
module EtagCaching
class Middleware
- RESERVED_WORDS = NamespaceValidator::WILDCARD_ROUTES.map { |word| "/#{word}/" }.join('|')
- ROUTES = [
- {
- regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z),
- name: 'issue_notes'
- },
- {
- regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/issues/\d+/rendered_title\z),
- name: 'issue_title'
- },
- {
- regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/pipelines\.json\z),
- name: 'project_pipelines'
- },
- {
- regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/commit/\s+/pipelines\.json\z),
- name: 'commit_pipelines'
- },
- {
- regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/merge_requests/new\.json\z),
- name: 'new_merge_request_pipelines'
- },
- {
- regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/merge_requests/\d+/pipelines\.json\z),
- name: 'merge_request_pipelines'
- }
- ].freeze
-
def initialize(app)
@app = app
end
def call(env)
- route = match_current_route(env)
+ route = Gitlab::EtagCaching::Router.match(env)
return @app.call(env) unless route
track_event(:etag_caching_middleware_used, route)
@@ -55,10 +27,6 @@ module Gitlab
private
- def match_current_route(env)
- ROUTES.find { |route| route[:regexp].match(env['PATH_INFO']) }
- end
-
def get_etag(env)
cache_key = env['PATH_INFO']
store = Gitlab::EtagCaching::Store.new
@@ -95,7 +63,7 @@ module Gitlab
end
def track_event(name, route)
- Gitlab::Metrics.add_event(name, endpoint: route[:name])
+ Gitlab::Metrics.add_event(name, endpoint: route.name)
end
end
end
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
new file mode 100644
index 00000000000..f6e4f279c06
--- /dev/null
+++ b/lib/gitlab/etag_caching/router.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module EtagCaching
+ class Router
+ Route = Struct.new(:regexp, :name)
+
+ RESERVED_WORDS = NamespaceValidator::WILDCARD_ROUTES.map { |word| "/#{word}/" }.join('|')
+ ROUTES = [
+ Gitlab::EtagCaching::Router::Route.new(
+ %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z),
+ 'issue_notes'
+ ),
+ Gitlab::EtagCaching::Router::Route.new(
+ %r(^(?!.*(#{RESERVED_WORDS})).*/issues/\d+/rendered_title\z),
+ 'issue_title'
+ ),
+ Gitlab::EtagCaching::Router::Route.new(
+ %r(^(?!.*(#{RESERVED_WORDS})).*/commit/\S+/pipelines\.json\z),
+ 'commit_pipelines'
+ ),
+ Gitlab::EtagCaching::Router::Route.new(
+ %r(^(?!.*(#{RESERVED_WORDS})).*/merge_requests/new\.json\z),
+ 'new_merge_request_pipelines'
+ ),
+ Gitlab::EtagCaching::Router::Route.new(
+ %r(^(?!.*(#{RESERVED_WORDS})).*/merge_requests/\d+/pipelines\.json\z),
+ 'merge_request_pipelines'
+ ),
+ Gitlab::EtagCaching::Router::Route.new(
+ %r(^(?!.*(#{RESERVED_WORDS})).*/pipelines\.json\z),
+ 'project_pipelines'
+ )
+ ].freeze
+
+ def self.match(env)
+ ROUTES.find { |route| route.regexp.match(env['PATH_INFO']) }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index e56eb0d3beb..98fd4e78126 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -8,7 +8,7 @@ module Gitlab
# the user. We load as much as we can for encoding detection
# (Linguist) and LFS pointer parsing. All other cases where we need full
# blob data should use load_all_data!.
- MAX_DATA_DISPLAY_SIZE = 10485760
+ MAX_DATA_DISPLAY_SIZE = 10.megabytes
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
@@ -153,7 +153,7 @@ module Gitlab
def lfs_size
if has_lfs_version_key?
size = data.match(/(?<=size )([0-9]+)/)
- return size[1] if size
+ return size[1].to_i if size
end
nil
diff --git a/lib/gitlab/git/encoding_helper.rb b/lib/gitlab/git/encoding_helper.rb
index e57d228e688..f918074cb14 100644
--- a/lib/gitlab/git/encoding_helper.rb
+++ b/lib/gitlab/git/encoding_helper.rb
@@ -40,7 +40,13 @@ module Gitlab
def encode_utf8(message)
detect = CharlockHolmes::EncodingDetector.detect(message)
if detect
- CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
+ begin
+ CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
+ rescue ArgumentError => e
+ Rails.logger.warn("Ignoring error converting #{detect[:encoding]} into UTF8: #{e.message}")
+
+ ''
+ end
else
clean(message)
end
diff --git a/lib/gitlab/git/env.rb b/lib/gitlab/git/env.rb
new file mode 100644
index 00000000000..0fdc57ec954
--- /dev/null
+++ b/lib/gitlab/git/env.rb
@@ -0,0 +1,38 @@
+module Gitlab
+ module Git
+ # Ephemeral (per request) storage for environment variables that some Git
+ # commands may need.
+ #
+ # For example, in pre-receive hooks, new objects are put in a temporary
+ # $GIT_OBJECT_DIRECTORY. Without it set, the new objects cannot be retrieved
+ # (this would break push rules for instance).
+ #
+ # This class is thread-safe via RequestStore.
+ class Env
+ WHITELISTED_GIT_VARIABLES = %w[
+ GIT_OBJECT_DIRECTORY
+ GIT_ALTERNATE_OBJECT_DIRECTORIES
+ ].freeze
+
+ def self.set(env)
+ return unless RequestStore.active?
+
+ RequestStore.store[:gitlab_git_env] = whitelist_git_env(env)
+ end
+
+ def self.all
+ return {} unless RequestStore.active?
+
+ RequestStore.fetch(:gitlab_git_env) { {} }
+ end
+
+ def self.[](key)
+ all[key]
+ end
+
+ def self.whitelist_git_env(env)
+ env.select { |key, _| WHITELISTED_GIT_VARIABLES.include?(key.to_s) }.with_indifferent_access
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb
index af1744c9c46..1add037fa5f 100644
--- a/lib/gitlab/git/index.rb
+++ b/lib/gitlab/git/index.rb
@@ -1,8 +1,12 @@
module Gitlab
module Git
class Index
+ IndexError = Class.new(StandardError)
+
DEFAULT_MODE = 0o100644
+ ACTIONS = %w(create create_dir update move delete).freeze
+
attr_reader :repository, :raw_index
def initialize(repository)
@@ -23,9 +27,8 @@ module Gitlab
def create(options)
options = normalize_options(options)
- file_entry = get(options[:file_path])
- if file_entry
- raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists")
+ if get(options[:file_path])
+ raise IndexError, "A file with this name already exists"
end
add_blob(options)
@@ -34,13 +37,12 @@ module Gitlab
def create_dir(options)
options = normalize_options(options)
- file_entry = get(options[:file_path])
- if file_entry
- raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists as a file")
+ if get(options[:file_path])
+ raise IndexError, "A file with this name already exists"
end
if dir_exists?(options[:file_path])
- raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists")
+ raise IndexError, "A directory with this name already exists"
end
options = options.dup
@@ -55,7 +57,7 @@ module Gitlab
file_entry = get(options[:file_path])
unless file_entry
- raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ raise IndexError, "A file with this name doesn't exist"
end
add_blob(options, mode: file_entry[:mode])
@@ -66,7 +68,11 @@ module Gitlab
file_entry = get(options[:previous_path])
unless file_entry
- raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ raise IndexError, "A file with this name doesn't exist"
+ end
+
+ if get(options[:file_path])
+ raise IndexError, "A file with this name already exists"
end
raw_index.remove(options[:previous_path])
@@ -77,9 +83,8 @@ module Gitlab
def delete(options)
options = normalize_options(options)
- file_entry = get(options[:file_path])
- unless file_entry
- raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ unless get(options[:file_path])
+ raise IndexError, "A file with this name doesn't exist"
end
raw_index.remove(options[:file_path])
@@ -95,10 +100,20 @@ module Gitlab
end
def normalize_path(path)
+ unless path
+ raise IndexError, "You must provide a file path"
+ end
+
pathname = Gitlab::Git::PathHelper.normalize_path(path.dup)
- if pathname.each_filename.include?('..')
- raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
+ pathname.each_filename do |segment|
+ if segment == '..'
+ raise IndexError, 'Path cannot include directory traversal'
+ end
+
+ unless segment =~ Gitlab::Regex.file_name_regex
+ raise IndexError, "Path #{Gitlab::Regex.file_name_regex_message}"
+ end
end
pathname.to_s
@@ -106,6 +121,10 @@ module Gitlab
def add_blob(options, mode: nil)
content = options[:content]
+ unless content
+ raise IndexError, "You must provide content"
+ end
+
content = Base64.decode64(content) if options[:encoding] == 'base64'
detect = CharlockHolmes::EncodingDetector.new.detect(content)
@@ -119,7 +138,7 @@ module Gitlab
raw_index.add(path: options[:file_path], oid: oid, mode: mode || DEFAULT_MODE)
rescue Rugged::IndexError => e
- raise Gitlab::Git::Repository::InvalidBlobName.new(e.message)
+ raise IndexError, e.message
end
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index fc473b2c21e..d7dac9f6149 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -8,6 +8,10 @@ module Gitlab
class Repository
include Gitlab::Git::Popen
+ ALLOWED_OBJECT_DIRECTORIES_VARIABLES = %w[
+ GIT_OBJECT_DIRECTORY
+ GIT_ALTERNATE_OBJECT_DIRECTORIES
+ ].freeze
SEARCH_CONTEXT_LINES = 3
NoRepository = Class.new(StandardError)
@@ -41,13 +45,15 @@ module Gitlab
# Default branch in the repository
def root_ref
- @root_ref ||= Gitlab::GitalyClient.migrate(:root_ref) do |is_enabled|
- if is_enabled
- gitaly_ref_client.default_branch_name
- else
- discover_default_branch
- end
- end
+ # NOTE: This feature is intentionally disabled until
+ # https://gitlab.com/gitlab-org/gitaly/issues/179 is resolved
+ # @root_ref ||= Gitlab::GitalyClient.migrate(:root_ref) do |is_enabled|
+ # if is_enabled
+ # gitaly_ref_client.default_branch_name
+ # else
+ @root_ref ||= discover_default_branch
+ # end
+ # end
rescue GRPC::BadStatus => e
raise CommandError.new(e)
end
@@ -58,7 +64,7 @@ module Gitlab
end
def rugged
- @rugged ||= Rugged::Repository.new(path)
+ @rugged ||= Rugged::Repository.new(path, alternates: alternate_object_directories)
rescue Rugged::RepositoryError, Rugged::OSError
raise NoRepository.new('no repository for such path')
end
@@ -66,13 +72,15 @@ module Gitlab
# Returns an Array of branch names
# sorted by name ASC
def branch_names
- Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled|
- if is_enabled
- gitaly_ref_client.branch_names
- else
- branches.map(&:name)
- end
- end
+ # Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled|
+ # NOTE: This feature is intentionally disabled until
+ # https://gitlab.com/gitlab-org/gitaly/issues/179 is resolved
+ # if is_enabled
+ # gitaly_ref_client.branch_names
+ # else
+ branches.map(&:name)
+ # end
+ # end
rescue GRPC::BadStatus => e
raise CommandError.new(e)
end
@@ -127,13 +135,15 @@ module Gitlab
# Returns an Array of tag names
def tag_names
- Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled|
- if is_enabled
- gitaly_ref_client.tag_names
- else
- rugged.tags.map { |t| t.name }
- end
- end
+ # Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled|
+ # NOTE: This feature is intentionally disabled until
+ # https://gitlab.com/gitlab-org/gitaly/issues/179 is resolved
+ # if is_enabled
+ # gitaly_ref_client.tag_names
+ # else
+ rugged.tags.map { |t| t.name }
+ # end
+ # end
rescue GRPC::BadStatus => e
raise CommandError.new(e)
end
@@ -454,17 +464,19 @@ module Gitlab
# Returns a RefName for a given SHA
def ref_name_for_sha(ref_path, sha)
- Gitlab::GitalyClient.migrate(:find_ref_name) do |is_enabled|
- if is_enabled
- gitaly_ref_client.find_ref_name(sha, ref_path)
- else
- args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
+ # NOTE: This feature is intentionally disabled until
+ # https://gitlab.com/gitlab-org/gitaly/issues/180 is resolved
+ # Gitlab::GitalyClient.migrate(:find_ref_name) do |is_enabled|
+ # if is_enabled
+ # gitaly_ref_client.find_ref_name(sha, ref_path)
+ # else
+ args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
- # Not found -> ["", 0]
- # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
- Gitlab::Popen.popen(args, @path).first.split.last
- end
- end
+ # Not found -> ["", 0]
+ # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
+ Gitlab::Popen.popen(args, @path).first.split.last
+ # end
+ # end
end
# Returns commits collection
@@ -978,6 +990,10 @@ module Gitlab
private
+ def alternate_object_directories
+ Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).compact
+ end
+
# Get the content of a blob for a given commit. If the blob is a commit
# (for submodules) then return the blob's OID.
def blob_content(commit, blob_name)
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index 79dd0cf7df2..a16b0ed76f4 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -1,41 +1,42 @@
module Gitlab
module Git
class RevList
- attr_reader :project, :env
-
- ALLOWED_VARIABLES = %w[GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES].freeze
-
- def initialize(oldrev, newrev, project:, env: nil)
- @project = project
- @env = env.presence || {}
- @args = [Gitlab.config.git.bin_path,
- "--git-dir=#{project.repository.path_to_repo}",
- "rev-list",
- "--max-count=1",
- oldrev,
- "^#{newrev}"]
+ attr_reader :oldrev, :newrev, :path_to_repo
+
+ def initialize(path_to_repo:, newrev:, oldrev: nil)
+ @oldrev = oldrev
+ @newrev = newrev
+ @path_to_repo = path_to_repo
end
- def execute
- Gitlab::Popen.popen(@args, nil, parse_environment_variables)
+ # This method returns an array of new references
+ def new_refs
+ execute([*base_args, newrev, '--not', '--all'])
end
- def valid?
- environment_variables.all? do |(name, value)|
- value.to_s.start_with?(project.repository.path_to_repo)
- end
+ # This methods returns an array of missed references
+ def missed_ref
+ execute([*base_args, '--max-count=1', oldrev, "^#{newrev}"])
end
private
- def parse_environment_variables
- return {} unless valid?
+ def execute(args)
+ output, status = Gitlab::Popen.popen(args, nil, Gitlab::Git::Env.all.stringify_keys)
+
+ unless status.zero?
+ raise "Got a non-zero exit code while calling out `#{args.join(' ')}`."
+ end
- environment_variables
+ output.split("\n")
end
- def environment_variables
- @environment_variables ||= env.slice(*ALLOWED_VARIABLES).compact
+ def base_args
+ [
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{path_to_repo}",
+ 'rev-list'
+ ]
end
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index eea2f206902..99724db8da2 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -18,13 +18,12 @@ module Gitlab
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
- def initialize(actor, project, protocol, authentication_abilities:, env: {})
+ def initialize(actor, project, protocol, authentication_abilities:)
@actor = actor
@project = project
@protocol = protocol
@authentication_abilities = authentication_abilities
@user_access = UserAccess.new(user, project: project)
- @env = env
end
def check(cmd, changes)
@@ -152,7 +151,6 @@ module Gitlab
change,
user_access: user_access,
project: project,
- env: @env,
skip_authorization: deploy_key?,
protocol: protocol
).exec
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index bcdf1b1faa8..c69676a1dac 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -15,7 +15,7 @@ module Gitlab
end
unless URI(address).scheme.in?(%w(tcp unix))
- raise "Unsupported Gitaly address: #{address.inspect}"
+ raise "Unsupported Gitaly address: #{address.inspect} does not use URL scheme 'tcp' or 'unix'"
end
@addresses[name] = address
diff --git a/lib/gitlab/markup_helper.rb b/lib/gitlab/markup_helper.rb
index dda371e6554..49285e35251 100644
--- a/lib/gitlab/markup_helper.rb
+++ b/lib/gitlab/markup_helper.rb
@@ -1,6 +1,11 @@
module Gitlab
module MarkupHelper
- module_function
+ extend self
+
+ MARKDOWN_EXTENSIONS = %w(mdown mkd mkdn md markdown).freeze
+ ASCIIDOC_EXTENSIONS = %w(adoc ad asciidoc).freeze
+ OTHER_EXTENSIONS = %w(textile rdoc org creole wiki mediawiki rst).freeze
+ EXTENSIONS = MARKDOWN_EXTENSIONS + ASCIIDOC_EXTENSIONS + OTHER_EXTENSIONS
# Public: Determines if a given filename is compatible with GitHub::Markup.
#
@@ -8,10 +13,7 @@ module Gitlab
#
# Returns boolean
def markup?(filename)
- gitlab_markdown?(filename) ||
- asciidoc?(filename) ||
- filename.downcase.end_with?(*%w(.textile .rdoc .org .creole .wiki
- .mediawiki .rst))
+ EXTENSIONS.include?(extension(filename))
end
# Public: Determines if a given filename is compatible with
@@ -21,7 +23,7 @@ module Gitlab
#
# Returns boolean
def gitlab_markdown?(filename)
- filename.downcase.end_with?(*%w(.mdown .mkd .mkdn .md .markdown))
+ MARKDOWN_EXTENSIONS.include?(extension(filename))
end
# Public: Determines if the given filename has AsciiDoc extension.
@@ -30,7 +32,7 @@ module Gitlab
#
# Returns boolean
def asciidoc?(filename)
- filename.downcase.end_with?(*%w(.adoc .ad .asciidoc))
+ ASCIIDOC_EXTENSIONS.include?(extension(filename))
end
# Public: Determines if the given filename is plain text.
@@ -39,12 +41,17 @@ module Gitlab
#
# Returns boolean
def plain?(filename)
- filename.downcase.end_with?('.txt') ||
- filename.casecmp('readme').zero?
+ extension(filename) == 'txt' || filename.casecmp('readme').zero?
end
def previewable?(filename)
markup?(filename)
end
+
+ private
+
+ def extension(filename)
+ File.extname(filename).downcase.delete('.')
+ end
end
end
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 857e0abf710..c6dfa4ad9bd 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -138,6 +138,11 @@ module Gitlab
@series_prefix ||= Sidekiq.server? ? 'sidekiq_' : 'rails_'
end
+ # Allow access from other metrics related middlewares
+ def self.current_transaction
+ Transaction.current
+ end
+
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
if enabled?
@@ -149,10 +154,5 @@ module Gitlab
new(udp: { host: host, port: port })
end
end
-
- # Allow access from other metrics related middlewares
- def self.current_transaction
- Transaction.current
- end
end
end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index f98481c6d3a..6e42d8941fb 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -148,7 +148,7 @@ module Gitlab
def build_new_user
user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true)
- Users::CreateService.new(nil, user_params).build
+ Users::BuildService.new(nil, user_params).execute
end
def user_attributes
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index e599dd4a656..08b061d5e31 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -73,22 +73,6 @@ module Gitlab
"can contain only letters, digits, '_', '-', '@', '+' and '.'."
end
- def file_path_regex
- @file_path_regex ||= /\A[[[:alnum:]]_\-\.\/\@]*\z/.freeze
- end
-
- def file_path_regex_message
- "can contain only letters, digits, '_', '-', '@' and '.'. Separate directories with a '/'."
- end
-
- def directory_traversal_regex
- @directory_traversal_regex ||= /\.{2}/.freeze
- end
-
- def directory_traversal_regex_message
- "cannot include directory traversal."
- end
-
def archive_formats_regex
# |zip|tar| tar.gz | tar.bz2 |
@archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
new file mode 100644
index 00000000000..6aca6db3123
--- /dev/null
+++ b/lib/gitlab/usage_data.rb
@@ -0,0 +1,65 @@
+module Gitlab
+ class UsageData
+ include Gitlab::CurrentSettings
+
+ class << self
+ def data(force_refresh: false)
+ Rails.cache.fetch('usage_data', force: force_refresh, expires_in: 2.weeks) { uncached_data }
+ end
+
+ def uncached_data
+ license_usage_data.merge(system_usage_data)
+ end
+
+ def to_json(force_refresh: false)
+ data(force_refresh: force_refresh).to_json
+ end
+
+ def system_usage_data
+ {
+ counts: {
+ boards: Board.count,
+ ci_builds: ::Ci::Build.count,
+ ci_pipelines: ::Ci::Pipeline.count,
+ ci_runners: ::Ci::Runner.count,
+ ci_triggers: ::Ci::Trigger.count,
+ deploy_keys: DeployKey.count,
+ deployments: Deployment.count,
+ environments: Environment.count,
+ groups: Group.count,
+ issues: Issue.count,
+ keys: Key.count,
+ labels: Label.count,
+ lfs_objects: LfsObject.count,
+ merge_requests: MergeRequest.count,
+ milestones: Milestone.count,
+ notes: Note.count,
+ pages_domains: PagesDomain.count,
+ projects: Project.count,
+ projects_prometheus_active: PrometheusService.active.count,
+ protected_branches: ProtectedBranch.count,
+ releases: Release.count,
+ services: Service.where(active: true).count,
+ snippets: Snippet.count,
+ todos: Todo.count,
+ uploads: Upload.count,
+ web_hooks: WebHook.count
+ }
+ }
+ end
+
+ def license_usage_data
+ usage_data = {
+ uuid: current_application_settings.uuid,
+ version: Gitlab::VERSION,
+ active_user_count: User.active.count,
+ recorded_at: Time.now,
+ mattermost_enabled: Gitlab.config.mattermost.enabled,
+ edition: 'CE'
+ }
+
+ usage_data
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/user_activities.rb b/lib/gitlab/user_activities.rb
new file mode 100644
index 00000000000..eb36ab9fded
--- /dev/null
+++ b/lib/gitlab/user_activities.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ class UserActivities
+ include Enumerable
+
+ KEY = 'users:activities'.freeze
+ BATCH_SIZE = 500
+
+ def self.record(key, time = Time.now)
+ Gitlab::Redis.with do |redis|
+ redis.hset(KEY, key, time.to_i)
+ end
+ end
+
+ def delete(*keys)
+ Gitlab::Redis.with do |redis|
+ redis.hdel(KEY, keys)
+ end
+ end
+
+ def each
+ cursor = 0
+ loop do
+ cursor, pairs =
+ Gitlab::Redis.with do |redis|
+ redis.hscan(KEY, cursor, count: BATCH_SIZE)
+ end
+
+ Hash[pairs].each { |pair| yield pair }
+
+ break if cursor == '0'
+ end
+ end
+ end
+end
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index d55923673b1..125a3d560d6 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -21,12 +21,7 @@ namespace :cache do
end
end
- desc "GitLab | Clear database cache (in the background)"
- task db: :environment do
- ClearDatabaseCacheWorker.perform_async
- end
-
- task all: [:db, :redis]
+ task all: [:redis]
end
task clear: 'cache:clear:redis'
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index a9a48f7188f..f41c73154f5 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -431,8 +431,7 @@ namespace :gitlab do
def check_repo_base_user_and_group
gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user
- gitlab_shell_owner_group = Gitlab.config.gitlab_shell.owner_group
- puts "Repo paths owned by #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group}?"
+ puts "Repo paths owned by #{gitlab_shell_ssh_user}:root, or #{gitlab_shell_ssh_user}:#{Gitlab.config.gitlab_shell.owner_group}?"
Gitlab.config.repositories.storages.each do |name, repository_storage|
repo_base_path = repository_storage['path']
@@ -443,15 +442,16 @@ namespace :gitlab do
break
end
- uid = uid_for(gitlab_shell_ssh_user)
- gid = gid_for(gitlab_shell_owner_group)
- if File.stat(repo_base_path).uid == uid && File.stat(repo_base_path).gid == gid
+ user_id = uid_for(gitlab_shell_ssh_user)
+ root_group_id = gid_for('root')
+ group_ids = [root_group_id, gid_for(Gitlab.config.gitlab_shell.owner_group)]
+ if File.stat(repo_base_path).uid == user_id && group_ids.include?(File.stat(repo_base_path).gid)
puts "yes".color(:green)
else
puts "no".color(:red)
- puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".color(:blue)
+ puts " User id for #{gitlab_shell_ssh_user}: #{user_id}. Groupd id for root: #{root_group_id}".color(:blue)
try_fixing_it(
- "sudo chown -R #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group} #{repo_base_path}"
+ "sudo chown -R #{gitlab_shell_ssh_user}:root #{repo_base_path}"
)
for_more_information(
see_installation_guide_section "GitLab Shell"
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 9f6cfe3957c..8079c6e416c 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -7,10 +7,10 @@ namespace :gitlab do
abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]")
end
- tag = "v#{Gitlab::GitalyClient.expected_server_version}"
+ version = Gitlab::GitalyClient.expected_server_version
repo = 'https://gitlab.com/gitlab-org/gitaly.git'
- checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir)
+ checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir)
_, status = Gitlab::Popen.popen(%w[which gmake])
command = status.zero? ? 'gmake' : 'make'
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index dd2fda54e62..95687066819 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -1,19 +1,18 @@
namespace :gitlab do
namespace :shell do
desc "GitLab | Install or upgrade gitlab-shell"
- task :install, [:tag, :repo] => :environment do |t, args|
+ task :install, [:repo] => :environment do |t, args|
warn_user_is_not_gitlab
default_version = Gitlab::Shell.version_required
- default_version_tag = "v#{default_version}"
- args.with_defaults(tag: default_version_tag, repo: 'https://gitlab.com/gitlab-org/gitlab-shell.git')
+ args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitlab-shell.git')
gitlab_url = Gitlab.config.gitlab.url
# gitlab-shell requires a / at the end of the url
gitlab_url += '/' unless gitlab_url.end_with?('/')
target_dir = Gitlab.config.gitlab_shell.path
- checkout_or_clone_tag(tag: default_version_tag, repo: args.repo, target_dir: target_dir)
+ checkout_or_clone_version(version: default_version, repo: args.repo, target_dir: target_dir)
# Make sure we're on the right tag
Dir.chdir(target_dir) do
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index cdba2262bc2..e3c9d3b491c 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -147,41 +147,30 @@ module Gitlab
Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home
end
- def checkout_or_clone_tag(tag:, repo:, target_dir:)
- if Dir.exist?(target_dir)
- checkout_tag(tag, target_dir)
- else
- clone_repo(repo, target_dir)
- end
+ def checkout_or_clone_version(version:, repo:, target_dir:)
+ version =
+ if version.starts_with?("=")
+ version.sub(/\A=/, '') # tag or branch
+ else
+ "v#{version}" # tag
+ end
- reset_to_tag(tag, target_dir)
+ clone_repo(repo, target_dir) unless Dir.exist?(target_dir)
+ checkout_version(version, target_dir)
+ reset_to_version(version, target_dir)
end
def clone_repo(repo, target_dir)
run_command!(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{target_dir}])
end
- def checkout_tag(tag, target_dir)
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch --tags --quiet])
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} checkout --quiet #{tag}])
+ def checkout_version(version, target_dir)
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch --quiet])
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} checkout --quiet #{version}])
end
- def reset_to_tag(tag_wanted, target_dir)
- tag =
- begin
- # First try to checkout without fetching
- # to avoid stalling tests if the Internet is down.
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
- rescue Gitlab::TaskFailedError
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
- end
-
- if tag
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{tag.strip}])
- else
- raise Gitlab::TaskFailedError
- end
+ def reset_to_version(version, target_dir)
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{version}])
end
end
end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index dbdfb335a5c..cb2adc81c9d 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -44,7 +44,7 @@ namespace :gitlab do
),
Template.new(
"https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
- /(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/
+ /(\.{1,2}|LICENSE|CONTRIBUTING.md|Pages|autodeploy|\.gitlab-ci.yml)\z/
)
].freeze
diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake
index baea94bf8ca..a00b02188cf 100644
--- a/lib/tasks/gitlab/workhorse.rake
+++ b/lib/tasks/gitlab/workhorse.rake
@@ -7,10 +7,10 @@ namespace :gitlab do
abort %(Please specify the directory where you want to install gitlab-workhorse:\n rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]")
end
- tag = "v#{Gitlab::Workhorse.version}"
+ version = Gitlab::Workhorse.version
repo = 'https://gitlab.com/gitlab-org/gitlab-workhorse.git'
- checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir)
+ checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir)
_, status = Gitlab::Popen.popen(%w[which gmake])
command = status.zero? ? 'gmake' : 'make'
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 15131fbf755..a9dad6a1bf0 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -52,7 +52,6 @@ class NewImporter < ::Gitlab::GithubImport::Importer
project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true)
- project.repository.remove_remote(project.import_type)
rescue => e
# Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
diff --git a/package.json b/package.json
index 6d4f99e33b3..e65f30eea77 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
{
"private": true,
"scripts": {
- "dev-server": "webpack-dev-server --config config/webpack.config.js",
- "eslint": "eslint --max-warnings 0 --ext .js .",
- "eslint-fix": "eslint --max-warnings 0 --ext .js --fix .",
- "eslint-report": "eslint --max-warnings 0 --ext .js --format html --output-file ./eslint-report.html .",
+ "dev-server": "nodemon --watch config/webpack.config.js -- ./node_modules/.bin/webpack-dev-server --config config/webpack.config.js",
+ "eslint": "eslint --max-warnings 0 --ext .js,.vue .",
+ "eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .",
+ "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .",
"karma": "karma start config/karma.config.js --single-run",
"karma-coverage": "BABEL_ENV=coverage karma start config/karma.config.js --single-run",
"karma-start": "karma start config/karma.config.js",
@@ -20,10 +20,12 @@
"bootstrap-sass": "^3.3.6",
"compression-webpack-plugin": "^0.3.2",
"core-js": "^2.4.1",
+ "css-loader": "^0.28.0",
"d3": "^3.5.11",
"document-register-element": "^1.3.0",
"dropzone": "^4.2.0",
"emoji-unicode-version": "^0.2.1",
+ "eslint-plugin-html": "^2.0.1",
"file-loader": "^0.11.1",
"jquery": "^2.2.1",
"jquery-ujs": "^1.2.1",
@@ -34,6 +36,7 @@
"pikaday": "^1.5.1",
"raphael": "^2.2.7",
"raw-loader": "^0.5.1",
+ "react-dev-utils": "^0.5.2",
"select2": "3.5.2-browserify",
"stats-webpack-plugin": "^0.4.3",
"three": "^0.84.0",
@@ -42,8 +45,10 @@
"timeago.js": "^2.0.5",
"underscore": "^1.8.3",
"visibilityjs": "^1.2.4",
- "vue": "^2.2.4",
+ "vue": "^2.2.6",
+ "vue-loader": "^11.3.4",
"vue-resource": "^0.9.3",
+ "vue-template-compiler": "^2.2.6",
"webpack": "^2.3.3",
"webpack-bundle-analyzer": "^2.3.0"
},
@@ -55,6 +60,7 @@
"eslint-plugin-filenames": "^1.1.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jasmine": "^2.1.0",
+ "eslint-plugin-promise": "^3.5.0",
"istanbul": "^0.4.5",
"jasmine-core": "^2.5.2",
"jasmine-jquery": "^2.1.1",
@@ -65,6 +71,7 @@
"karma-phantomjs-launcher": "^1.0.2",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^2.0.2",
+ "nodemon": "^1.11.0",
"webpack-dev-server": "^2.4.2"
}
}
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 45db7a92fa4..7ce4e9009f5 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -11,7 +11,7 @@ module QA
end
def go_to_admin_area
- within_user_menu { click_link 'Admin Area' }
+ within_user_menu { click_link 'Admin area' }
end
def sign_out
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 6e3f76b8399..6cacb81b8bc 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -15,21 +15,12 @@ retry() {
return 1
}
-if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
- cp config/database.yml.mysql config/database.yml
- sed -i 's/username:.*/username: root/g' config/database.yml
- sed -i 's/password:.*/password:/g' config/database.yml
- sed -i 's/# socket:.*/host: mysql/g' config/database.yml
+cp config/database.yml.mysql config/database.yml
+sed -i 's/username:.*/username: root/g' config/database.yml
+sed -i 's/password:.*/password:/g' config/database.yml
+sed -i 's/# socket:.*/host: mysql/g' config/database.yml
- cp config/resque.yml.example config/resque.yml
- sed -i 's/localhost/redis/g' config/resque.yml
+cp config/resque.yml.example config/resque.yml
+sed -i 's/localhost/redis/g' config/resque.yml
- export FLAGS="--path vendor --retry 3 --quiet"
-else
- rnd=$(awk 'BEGIN { srand() ; printf("%d\n",rand()*5) }')
- export PATH="$HOME/bin:/usr/local/bin:/usr/bin:/bin"
- cp config/database.yml.mysql config/database.yml
- sed "s/username\:.*$/username\: runner/" -i config/database.yml
- sed "s/password\:.*$/password\: 'password'/" -i config/database.yml
- sed "s/gitlabhq_test/gitlabhq_test_$rnd/" -i config/database.yml
-fi
+export FLAGS="--path vendor --retry 3 --quiet"
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 5dd8f66343f..2565622f8df 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -3,12 +3,49 @@ require 'spec_helper'
describe Admin::ApplicationSettingsController do
include StubENV
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
let(:admin) { create(:admin) }
+ let(:user) { create(:user)}
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
+ describe 'GET #usage_data with no access' do
+ before do
+ sign_in(user)
+ end
+
+ it 'returns 404' do
+ get :usage_data, format: :html
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe 'GET #usage_data' do
+ before do
+ sign_in(admin)
+ end
+
+ it 'returns HTML data' do
+ get :usage_data, format: :html
+
+ expect(response.body).to start_with('<span')
+ expect(response.status).to eq(200)
+ end
+
+ it 'returns JSON data' do
+ get :usage_data, format: :json
+
+ body = JSON.parse(response.body)
+ expect(body["version"]).to eq(Gitlab::VERSION)
+ expect(body).to include('counts')
+ expect(response.status).to eq(200)
+ end
+ end
+
describe 'PUT #update' do
before do
sign_in(admin)
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 84db26a958a..c29b2fe8946 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -22,4 +22,28 @@ describe Admin::GroupsController do
expect(response).to redirect_to(admin_groups_path)
end
end
+
+ describe 'PUT #members_update' do
+ let(:group_user) { create(:user) }
+
+ it 'adds user to members' do
+ put :members_update, id: group,
+ user_ids: group_user.id,
+ access_level: Gitlab::Access::GUEST
+
+ expect(response).to set_flash.to 'Users were successfully added.'
+ expect(response).to redirect_to(admin_group_path(group))
+ expect(group.users).to include group_user
+ end
+
+ it 'adds no user to members' do
+ put :members_update, id: group,
+ user_ids: '',
+ access_level: Gitlab::Access::GUEST
+
+ expect(response).to set_flash.to 'No users specified.'
+ expect(response).to redirect_to(admin_group_path(group))
+ expect(group.users).not_to include group_user
+ end
+ end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 3e9f272a0d8..0fd09d156c4 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -106,7 +106,7 @@ describe Projects::BlobController do
namespace_id: project.namespace,
project_id: project,
id: 'master/CHANGELOG',
- target_branch: 'master',
+ branch_name: 'master',
content: 'Added changes',
commit_message: 'Update CHANGELOG'
}
@@ -178,7 +178,7 @@ describe Projects::BlobController do
context 'when editing on the original repository' do
it "redirects to forked project new merge request" do
- default_params[:target_branch] = "fork-test-1"
+ default_params[:branch_name] = "fork-test-1"
default_params[:create_merge_request] = 1
put :update, default_params
diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/builds_controller_spec.rb
index 13208d21918..faf3770f5e9 100644
--- a/spec/controllers/projects/builds_controller_spec.rb
+++ b/spec/controllers/projects/builds_controller_spec.rb
@@ -60,7 +60,7 @@ describe Projects::BuildsController do
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
- expect(json_response['favicon']).to eq status.favicon
+ expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico"
end
end
end
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 6a6e9bf378a..05999431d8f 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -127,7 +127,7 @@ describe Projects::LabelsController do
context 'group owner' do
before do
- GroupMember.add_users_to_group(group, [user], :owner)
+ GroupMember.add_users(group, [user], :owner)
end
it 'gives access' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 1739d40ab88..cc393bd24f2 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1208,7 +1208,7 @@ describe Projects::MergeRequestsController do
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
- expect(json_response['favicon']).to eq status.favicon
+ expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico"
end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index d8f9bfd0d37..d9192177a06 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -86,7 +86,7 @@ describe Projects::PipelinesController do
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
- expect(json_response['favicon']).to eq status.favicon
+ expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico"
end
end
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 416eaa0037e..a4b4392d7cc 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -55,7 +55,7 @@ describe Projects::ProjectMembersController do
user_ids: '',
access_level: Gitlab::Access::GUEST
- expect(response).to set_flash.to 'No users or groups specified.'
+ expect(response).to set_flash.to 'No users specified.'
expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project))
end
end
@@ -225,7 +225,7 @@ describe Projects::ProjectMembersController do
id: member
expect(response).to redirect_to(
- namespace_project_project_members_path(project.namespace, project)
+ namespace_project_settings_members_path(project.namespace, project)
)
expect(project.members).to include member
end
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index ab94e292e48..a43dad5756d 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -97,29 +97,29 @@ describe Projects::TreeController do
project_id: project,
id: 'master',
dir_name: path,
- target_branch: target_branch,
+ branch_name: branch_name,
commit_message: 'Test commit message')
end
context 'successful creation' do
let(:path) { 'files/new_dir'}
- let(:target_branch) { 'master-test'}
+ let(:branch_name) { 'master-test'}
it 'redirects to the new directory' do
expect(subject).
- to redirect_to("/#{project.path_with_namespace}/tree/#{target_branch}/#{path}")
+ to redirect_to("/#{project.path_with_namespace}/tree/#{branch_name}/#{path}")
expect(flash[:notice]).to eq('The directory has been successfully created.')
end
end
context 'unsuccessful creation' do
let(:path) { 'README.md' }
- let(:target_branch) { 'master'}
+ let(:branch_name) { 'master'}
it 'does not allow overwriting of existing files' do
expect(subject).
to redirect_to("/#{project.path_with_namespace}/tree/master")
- expect(flash[:alert]).to eq('Directory already exists as a file')
+ expect(flash[:alert]).to eq('A file with this name already exists')
end
end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 9c16a7bc08b..038132cffe0 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -16,7 +16,9 @@ describe SessionsController do
end
end
- context 'when using valid password' do
+ context 'when using valid password', :redis do
+ include UserActivitiesHelpers
+
let(:user) { create(:user) }
it 'authenticates user correctly' do
@@ -37,6 +39,12 @@ describe SessionsController do
subject.sign_out user
end
end
+
+ it 'updates the user activity' do
+ expect do
+ post(:create, user: { login: user.username, password: user.password })
+ end.to change { user_activity(user) }
+ end
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 361f9dac191..253a025af48 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -40,6 +40,10 @@ FactoryGirl.define do
state :closed
end
+ trait :opened do
+ state :opened
+ end
+
trait :reopened do
state :reopened
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 90c35e2c7f8..93f4903119c 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -40,6 +40,7 @@ FactoryGirl.define do
transient do
line_number 14
+ diff_refs { noteable.try(:diff_refs) }
end
position do
@@ -48,7 +49,7 @@ FactoryGirl.define do
new_path: "files/ruby/popen.rb",
old_line: nil,
new_line: line_number,
- diff_refs: noteable.try(:diff_refs)
+ diff_refs: diff_refs
)
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 87a8f62687a..9d205104ebe 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -109,7 +109,7 @@ describe "Admin::Projects", feature: true do
expect(page).to have_content('Developer')
end
- find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
+ find(:css, '.content-list li', text: current_user.name).find(:css, 'a.btn-remove').click
expect(page).not_to have_selector(:css, '.content-list')
end
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index fa5524e18d8..4c9adcabe34 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -1,27 +1,49 @@
require 'spec_helper'
feature 'Dashboard shortcuts', feature: true, js: true do
- before do
- login_as :user
- visit root_dashboard_path
- end
+ context 'logged in' do
+ before do
+ login_as :user
+ visit root_dashboard_path
+ end
+
+ scenario 'Navigate to tabs' do
+ find('body').native.send_keys([:shift, 'P'])
+
+ check_page_title('Projects')
+
+ find('body').native.send_key([:shift, 'I'])
+
+ check_page_title('Issues')
- scenario 'Navigate to tabs' do
- find('body').native.send_keys([:shift, 'P'])
+ find('body').native.send_key([:shift, 'M'])
+
+ check_page_title('Merge Requests')
+
+ find('body').native.send_keys([:shift, 'T'])
+
+ check_page_title('Todos')
+ end
+ end
- check_page_title('Projects')
+ context 'logged out' do
+ before do
+ visit explore_root_path
+ end
- find('body').native.send_key([:shift, 'I'])
+ scenario 'Navigate to tabs' do
+ find('body').native.send_keys([:shift, 'P'])
- check_page_title('Issues')
+ expect(page).to have_content('No projects found')
- find('body').native.send_key([:shift, 'M'])
+ find('body').native.send_keys([:shift, 'G'])
- check_page_title('Merge Requests')
+ expect(page).to have_content('No public groups')
- find('body').native.send_keys([:shift, 'T'])
+ find('body').native.send_keys([:shift, 'S'])
- check_page_title('Todos')
+ expect(page).to have_selector('.snippets-list-holder')
+ end
end
def check_page_title(title)
diff --git a/spec/features/discussion_comments/commit_spec.rb b/spec/features/discussion_comments/commit_spec.rb
new file mode 100644
index 00000000000..96e0b78f6b9
--- /dev/null
+++ b/spec/features/discussion_comments/commit_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Merge Request', :feature, :js do
+ include RepoHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ project.add_master(user)
+ login_as(user)
+
+ visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
+ end
+
+ it_behaves_like 'discussion comments', 'commit'
+end
diff --git a/spec/features/discussion_comments/issue_spec.rb b/spec/features/discussion_comments/issue_spec.rb
new file mode 100644
index 00000000000..ccc9efccd18
--- /dev/null
+++ b/spec/features/discussion_comments/issue_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Issue', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+
+ before do
+ project.add_master(user)
+ login_as(user)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it_behaves_like 'discussion comments', 'issue'
+end
diff --git a/spec/features/discussion_comments/merge_request_spec.rb b/spec/features/discussion_comments/merge_request_spec.rb
new file mode 100644
index 00000000000..f99ebeb9cd9
--- /dev/null
+++ b/spec/features/discussion_comments/merge_request_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Merge Request', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ project.add_master(user)
+ login_as(user)
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it_behaves_like 'discussion comments', 'merge request'
+end
diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb
new file mode 100644
index 00000000000..19a306511b2
--- /dev/null
+++ b/spec/features/discussion_comments/snippets_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Issue', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
+
+ before do
+ project.add_master(user)
+ login_as(user)
+
+ visit namespace_project_snippet_path(project.namespace, project, snippet)
+ end
+
+ it_behaves_like 'discussion comments', 'snippet'
+end
diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/list_spec.rb
index 14c193f7450..543879bd21d 100644
--- a/spec/features/groups/members/list_spec.rb
+++ b/spec/features/groups/members/list_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Groups members list', feature: true do
+ include Select2Helper
+
let(:user1) { create(:user, name: 'John Doe') }
let(:user2) { create(:user, name: 'Mary Jane') }
let(:group) { create(:group) }
@@ -30,7 +32,7 @@ feature 'Groups members list', feature: true do
expect(second_row).to be_blank
end
- it 'updates user to owner level', :js do
+ scenario 'update user to owner level', :js do
group.add_owner(user1)
group.add_developer(user2)
@@ -38,13 +40,52 @@ feature 'Groups members list', feature: true do
page.within(second_row) do
click_button('Developer')
-
click_link('Owner')
expect(page).to have_button('Owner')
end
end
+ scenario 'add user to group', :js do
+ group.add_owner(user1)
+
+ visit group_group_members_path(group)
+
+ add_user(user2.id, 'Reporter')
+
+ page.within(second_row) do
+ expect(page).to have_content(user2.name)
+ expect(page).to have_button('Reporter')
+ end
+ end
+
+ scenario 'add yourself to group when already an owner', :js do
+ group.add_owner(user1)
+
+ visit group_group_members_path(group)
+
+ add_user(user1.id, 'Reporter')
+
+ page.within(first_row) do
+ expect(page).to have_content(user1.name)
+ expect(page).to have_content('Owner')
+ end
+ end
+
+ scenario 'invite user to group', :js do
+ group.add_owner(user1)
+
+ visit group_group_members_path(group)
+
+ add_user('test@example.com', 'Reporter')
+
+ page.within(second_row) do
+ expect(page).to have_content('test@example.com')
+ expect(page).to have_content('Invited')
+ expect(page).to have_button('Reporter')
+ end
+ end
+
def first_row
page.all('ul.content-list > li')[0]
end
@@ -52,4 +93,13 @@ feature 'Groups members list', feature: true do
def second_row
page.all('ul.content-list > li')[1]
end
+
+ def add_user(id, role)
+ page.within ".users-group-form" do
+ select2(id, from: "#user_ids", multiple: true)
+ select(role, from: "access_level")
+ end
+
+ click_button "Add to group"
+ end
end
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
new file mode 100644
index 00000000000..daa2c6afd63
--- /dev/null
+++ b/spec/features/groups/milestone_spec.rb
@@ -0,0 +1,36 @@
+require 'rails_helper'
+
+feature 'Group milestones', :feature, :js do
+ let(:group) { create(:group) }
+ let!(:project) { create(:project_empty_repo, group: group) }
+ let(:user) { create(:group_member, :master, user: create(:user), group: group ).user }
+
+ before do
+ Timecop.freeze
+
+ login_as(user)
+ end
+
+ after do
+ Timecop.return
+ end
+
+ context 'create a milestone' do
+ before do
+ visit new_group_milestone_path(group)
+ end
+
+ it 'creates milestone with start date' do
+ fill_in 'Title', with: 'testing'
+ find('#milestone_start_date').click
+
+ page.within(find('.pika-single')) do
+ click_button '1'
+ end
+
+ click_button 'Create milestone'
+
+ expect(find('.start_date')).to have_content(Date.today.at_beginning_of_month.strftime('%b %-d, %Y'))
+ end
+ end
+end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 7b9d4534ada..85585587fb1 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -120,6 +120,20 @@ feature 'Issue Sidebar', feature: true do
end
end
+ context 'as a allowed mobile user', js: true do
+ before do
+ project.team << [user, :developer]
+ resize_screen_xs
+ visit_issue(project, issue)
+ end
+
+ context 'mobile sidebar' do
+ it 'collapses the sidebar for small screens' do
+ expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed')
+ end
+ end
+ end
+
context 'as a guest' do
before do
project.team << [user, :guest]
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index f5cfe2d666e..378f6de1a78 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -1,17 +1,15 @@
require 'spec_helper'
-feature 'Issue notes polling' do
- let!(:project) { create(:project, :public) }
- let!(:issue) { create(:issue, project: project) }
+feature 'Issue notes polling', :feature, :js do
+ let(:project) { create(:empty_project, :public) }
+ let(:issue) { create(:issue, project: project) }
- background do
+ before do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- scenario 'Another user adds a comment to an issue', js: true do
- note = create(:note, noteable: issue, project: project,
- note: 'Looks good!')
-
+ it 'should display the new comment' do
+ note = create(:note, noteable: issue, project: project, note: 'Looks good!')
page.execute_script('notes.refresh();')
expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!')
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index e3213d24f6a..55eca187f6c 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -414,7 +414,8 @@ describe 'Issues', feature: true do
it 'will not send ajax request when no data is changed' do
page.within '.labels' do
click_link 'Edit'
- first('.dropdown-menu-close').click
+
+ find('.dropdown-menu-close', match: :first).click
expect(page).not_to have_selector('.block-loading')
end
@@ -601,10 +602,10 @@ describe 'Issues', feature: true do
expect(page.find_field("issue_description").value).to have_content 'banana_sample'
end
- it 'adds double newline to end of attachment markdown' do
+ it "doesn't add double newline to end of a single attachment markdown" do
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
- expect(page.find_field("issue_description").value).to match /\n\n$/
+ expect(page.find_field("issue_description").value).not_to match /\n\n$/
end
end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index 3a4ec07b2b0..16b09933bda 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -20,13 +20,13 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Target branch')
first('.js-source-branch').click
- first('.dropdown-source-branch .dropdown-content a', text: 'v1.1.0').click
+ find('.dropdown-source-branch .dropdown-content a', match: :first).click
expect(page).to have_content "b83d6e3"
end
it 'selects the target branch sha when a tag with the same name exists' do
- visit namespace_project_merge_requests_path(project.namespace, project)
+ visit namespace_project_merge_requests_path(project.namespace, project)
click_link 'New merge request'
@@ -46,8 +46,8 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Source branch')
expect(page).to have_content('Target branch')
- first('.js-source-branch').click
- first('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch').click
+ find('.js-source-branch', match: :first).click
+ find('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch', match: :first).click
click_button "Compare branches"
click_link "Changes"
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index 88d28b649a4..0e23c3a8849 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -198,6 +198,8 @@ feature 'Diff notes resolve', feature: true, js: true do
it 'does not mark discussion as resolved when resolving single note' do
page.first '.diff-content .note' do
first('.line-resolve-btn').click
+
+ expect(page).to have_selector('.note-action-button .loading')
expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
end
diff --git a/spec/features/merge_requests/diff_notes_spec.rb b/spec/features/merge_requests/diff_notes_spec.rb
deleted file mode 100644
index 06fad1007e8..00000000000
--- a/spec/features/merge_requests/diff_notes_spec.rb
+++ /dev/null
@@ -1,238 +0,0 @@
-require 'spec_helper'
-
-feature 'Diff notes', js: true, feature: true do
- include WaitForAjax
-
- before do
- login_as :admin
- @merge_request = create(:merge_request)
- @project = @merge_request.source_project
- end
-
- context 'merge request diffs' do
- let(:comment_button_class) { '.add-diff-note' }
- let(:notes_holder_input_class) { 'js-temp-notes-holder' }
- let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' }
- let(:test_note_comment) { 'this is a test note!' }
-
- context 'when hovering over a parallel view diff file' do
- before(:each) do
- visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'parallel')
- end
-
- context 'with an old line on the left and no line on the right' do
- it 'should allow commenting on the left side' do
- should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left')
- end
-
- it 'should not allow commenting on the right side' do
- should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
- end
- end
-
- context 'with no line on the left and a new line on the right' do
- it 'should not allow commenting on the left side' do
- should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
- end
-
- it 'should allow commenting on the right side' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
- end
- end
-
- context 'with an old line on the left and a new line on the right' do
- it 'should allow commenting on the left side' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
- end
-
- it 'should allow commenting on the right side' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
- end
- end
-
- context 'with an unchanged line on the left and an unchanged line on the right' do
- it 'should allow commenting on the left side' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
- end
-
- it 'should allow commenting on the right side' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
- end
- end
-
- context 'with a match line' do
- it 'should not allow commenting on the left side' do
- should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left')
- end
-
- it 'should not allow commenting on the right side' do
- should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right')
- end
- end
-
- context 'with an unfolded line' do
- before(:each) do
- find('.js-unfold', match: :first).click
- wait_for_ajax
- end
-
- # The first `.js-unfold` unfolds upwards, therefore the first
- # `.line_holder` will be an unfolded line.
- let(:line_holder) { first('.line_holder[id="1"]') }
-
- it 'should not allow commenting on the left side' do
- should_not_allow_commenting(line_holder, 'left')
- end
-
- it 'should not allow commenting on the right side' do
- should_not_allow_commenting(line_holder, 'right')
- end
- end
- end
-
- context 'when hovering over an inline view diff file' do
- before do
- visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
- end
-
- context 'with a new line' do
- it 'should allow commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
- end
- end
-
- context 'with an old line' do
- it 'should allow commenting' do
- should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
- end
- end
-
- context 'with an unchanged line' do
- it 'should allow commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
- end
- end
-
- context 'with a match line' do
- it 'should not allow commenting' do
- should_not_allow_commenting(find('.match', match: :first))
- end
- end
-
- context 'with an unfolded line' do
- before(:each) do
- find('.js-unfold', match: :first).click
- wait_for_ajax
- end
-
- # The first `.js-unfold` unfolds upwards, therefore the first
- # `.line_holder` will be an unfolded line.
- let(:line_holder) { first('.line_holder[id="1"]') }
-
- it 'should not allow commenting' do
- should_not_allow_commenting line_holder
- end
- end
-
- context 'when hovering over a diff discussion' do
- before do
- visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
- visit namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- end
-
- it 'should not allow commenting' do
- should_not_allow_commenting(find('.line_holder', match: :first))
- end
- end
- end
-
- context 'when the MR only supports legacy diff notes' do
- before do
- @merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
- visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
- end
-
- context 'with a new line' do
- it 'should allow commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
- end
- end
-
- context 'with an old line' do
- it 'should allow commenting' do
- should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
- end
- end
-
- context 'with an unchanged line' do
- it 'should allow commenting' do
- should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
- end
- end
-
- context 'with a match line' do
- it 'should not allow commenting' do
- should_not_allow_commenting(find('.match', match: :first))
- end
- end
- end
-
- def should_allow_commenting(line_holder, diff_side = nil)
- line = get_line_components(line_holder, diff_side)
- line[:content].hover
- expect(line[:num]).to have_css comment_button_class
-
- comment_on_line(line_holder, line)
-
- assert_comment_persistence(line_holder)
- end
-
- def should_not_allow_commenting(line_holder, diff_side = nil)
- line = get_line_components(line_holder, diff_side)
- line[:content].hover
- expect(line[:num]).not_to have_css comment_button_class
- end
-
- def get_line_components(line_holder, diff_side = nil)
- if diff_side.nil?
- get_inline_line_components(line_holder)
- else
- get_parallel_line_components(line_holder, diff_side)
- end
- end
-
- def get_inline_line_components(line_holder)
- { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
- end
-
- def get_parallel_line_components(line_holder, diff_side = nil)
- side_index = diff_side == 'left' ? 0 : 1
- # Wait for `.line_content`
- line_holder.find('.line_content', match: :first)
- # Wait for `.diff-line-num`
- line_holder.find('.diff-line-num', match: :first)
- { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
- end
-
- def comment_on_line(line_holder, line)
- line[:num].find(comment_button_class).trigger 'click'
- line_holder.find(:xpath, notes_holder_input_xpath)
-
- notes_holder_input = line_holder.find(:xpath, notes_holder_input_xpath)
- expect(notes_holder_input[:class]).to include(notes_holder_input_class)
-
- notes_holder_input.fill_in 'note[note]', with: test_note_comment
- click_button 'Comment'
- wait_for_ajax
- end
-
- def assert_comment_persistence(line_holder)
- expect(line_holder).to have_xpath notes_holder_input_xpath
-
- notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath)
- expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class)
- expect(notes_holder_saved).to have_content test_note_comment
- end
- end
-end
diff --git a/spec/features/merge_requests/discussion_spec.rb b/spec/features/merge_requests/discussion_spec.rb
new file mode 100644
index 00000000000..f59d0faa274
--- /dev/null
+++ b/spec/features/merge_requests/discussion_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+feature 'Merge Request Discussions', feature: true do
+ before do
+ login_as :admin
+ end
+
+ context "Diff discussions" do
+ let(:merge_request) { create(:merge_request, importing: true) }
+ let(:project) { merge_request.source_project }
+ let!(:old_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: outdated_diff_refs) }
+ let!(:new_merge_request_diff) { merge_request.merge_request_diffs.create }
+
+ let!(:outdated_discussion) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position).to_discussion }
+ let!(:active_discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+
+ let(:outdated_position) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: outdated_diff_refs
+ )
+ end
+
+ let(:outdated_diff_refs) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs }
+
+ before(:each) do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ context 'active discussions' do
+ it 'shows a link to the diff' do
+ within(".discussion[data-discussion-id='#{active_discussion.id}']") do
+ path = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: active_discussion.line_code)
+ expect(page).to have_link('the diff', href: path)
+ end
+ end
+ end
+
+ context 'outdated discussions' do
+ it 'shows a link to the outdated diff' do
+ within(".discussion[data-discussion-id='#{outdated_discussion.id}']") do
+ path = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, diff_id: old_merge_request_diff.id, anchor: outdated_discussion.line_code)
+ expect(page).to have_link('an outdated diff', href: path)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
index 646e7bab265..cd540ca113a 100644
--- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
@@ -89,6 +89,19 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
it_behaves_like 'Merge when pipeline succeeds activator'
end
end
+
+ describe 'enabling Merge when pipeline succeeds via dropdown' do
+ it 'activates the Merge when pipeline succeeds feature' do
+ click_button 'Select merge moment'
+ within('.js-merge-dropdown') do
+ click_link 'Merge when pipeline succeeds'
+ end
+
+ expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
+ expect(page).to have_content "The source branch will not be removed."
+ expect(page).to have_link "Cancel automatic merge"
+ end
+ end
end
context 'when merge when pipeline succeeds is enabled' do
diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
new file mode 100644
index 00000000000..7756202e3f5
--- /dev/null
+++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
@@ -0,0 +1,294 @@
+require 'spec_helper'
+
+feature 'Merge requests > User posts diff notes', :js do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.source_project }
+
+ before do
+ project.add_developer(user)
+ login_as(user)
+ end
+
+ let(:comment_button_class) { '.add-diff-note' }
+ let(:notes_holder_input_class) { 'js-temp-notes-holder' }
+ let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' }
+ let(:test_note_comment) { 'this is a test note!' }
+
+ context 'when hovering over a parallel view diff file' do
+ before do
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'parallel')
+ end
+
+ context 'with an old line on the left and no line on the right' do
+ it 'allows commenting on the left side' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'does not allow commenting on the right side' do
+ should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with no line on the left and a new line on the right' do
+ it 'does not allow commenting on the left side' do
+ should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'allows commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an old line on the left and a new line on the right' do
+ it 'allows commenting on the left side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'allows commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an unchanged line on the left and an unchanged line on the right' do
+ it 'allows commenting on the left side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
+ end
+
+ it 'allows commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
+ end
+ end
+
+ 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')
+ end
+
+ it 'does not allow commenting on the right side' do
+ should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an unfolded line' do
+ before(:each) do
+ find('.js-unfold', match: :first).click
+ wait_for_ajax
+ end
+
+ # The first `.js-unfold` unfolds upwards, therefore the first
+ # `.line_holder` will be an unfolded line.
+ let(:line_holder) { first('.line_holder[id="1"]') }
+
+ it 'does not allow commenting on the left side' do
+ should_not_allow_commenting(line_holder, 'left')
+ end
+
+ it 'does not allow commenting on the right side' do
+ should_not_allow_commenting(line_holder, 'right')
+ end
+ end
+ end
+
+ context 'when hovering over an inline view diff file' do
+ before do
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline')
+ end
+
+ context 'with a new line' do
+ it 'allows commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ end
+ end
+
+ context 'with an old line' do
+ it 'allows commenting' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ end
+ end
+
+ context 'with an unchanged line' do
+ it 'allows commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ end
+ end
+
+ context 'with a match line' do
+ it 'does not allow commenting' do
+ should_not_allow_commenting(find('.match', match: :first))
+ end
+ end
+
+ context 'with an unfolded line' do
+ before(:each) do
+ find('.js-unfold', match: :first).click
+ wait_for_ajax
+ end
+
+ # The first `.js-unfold` unfolds upwards, therefore the first
+ # `.line_holder` will be an unfolded line.
+ let(:line_holder) { first('.line_holder[id="1"]') }
+
+ it 'does not allow commenting' do
+ should_not_allow_commenting line_holder
+ end
+ end
+
+ context 'when hovering over a diff discussion' do
+ before do
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline')
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not allow commenting' do
+ should_not_allow_commenting(find('.line_holder', match: :first))
+ end
+ end
+ end
+
+ context 'when cancelling the comment addition' do
+ before do
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline')
+ end
+
+ context 'with a new line' do
+ it 'allows dismissing a comment' do
+ should_allow_dismissing_a_comment(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ end
+ end
+ end
+
+ describe 'with muliple note forms' do
+ before do
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline')
+ click_diff_line(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ click_diff_line(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ end
+
+ describe 'posting a note' do
+ it '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)
+ expect(page).to have_css('.notes_holder .note', count: 1)
+ expect(page).to have_css('.js-temp-notes-holder', count: 1)
+ expect(page).to have_button('Reply...')
+ end
+ end
+ end
+
+ context 'when the MR only supports legacy diff notes' do
+ before do
+ merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline')
+ end
+
+ context 'with a new line' do
+ it 'allows commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ end
+ end
+
+ context 'with an old line' do
+ it 'allows commenting' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ end
+ end
+
+ context 'with an unchanged line' do
+ it 'allows commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ end
+ end
+
+ context 'with a match line' do
+ it 'does not allow commenting' do
+ should_not_allow_commenting(find('.match', match: :first))
+ end
+ end
+ end
+
+ def should_allow_commenting(line_holder, diff_side = nil, asset_form_reset: true)
+ write_comment_on_line(line_holder, diff_side)
+
+ click_button 'Comment'
+ wait_for_ajax
+
+ assert_comment_persistence(line_holder, asset_form_reset: asset_form_reset)
+ end
+
+ def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
+ write_comment_on_line(line_holder, diff_side)
+
+ find('.js-close-discussion-note-form').trigger('click')
+
+ assert_comment_dismissal(line_holder)
+ end
+
+ def should_not_allow_commenting(line_holder, diff_side = nil)
+ line = get_line_components(line_holder, diff_side)
+ line[:content].hover
+ expect(line[:num]).not_to have_css comment_button_class
+ end
+
+ def get_line_components(line_holder, diff_side = nil)
+ if diff_side.nil?
+ get_inline_line_components(line_holder)
+ else
+ get_parallel_line_components(line_holder, diff_side)
+ end
+ end
+
+ def get_inline_line_components(line_holder)
+ { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
+ end
+
+ def get_parallel_line_components(line_holder, diff_side = nil)
+ side_index = diff_side == 'left' ? 0 : 1
+ # Wait for `.line_content`
+ line_holder.find('.line_content', match: :first)
+ # Wait for `.diff-line-num`
+ line_holder.find('.diff-line-num', match: :first)
+ { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
+ end
+
+ def click_diff_line(line_holder, diff_side = nil)
+ line = get_line_components(line_holder, diff_side)
+ line[:content].hover
+
+ expect(line[:num]).to have_css comment_button_class
+
+ line[:num].find(comment_button_class).trigger 'click'
+ end
+
+ def write_comment_on_line(line_holder, diff_side)
+ click_diff_line(line_holder, diff_side)
+
+ notes_holder_input = line_holder.find(:xpath, notes_holder_input_xpath)
+
+ expect(notes_holder_input[:class]).to include(notes_holder_input_class)
+
+ notes_holder_input.fill_in 'note[note]', with: test_note_comment
+ end
+
+ def assert_comment_persistence(line_holder, asset_form_reset:)
+ notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath)
+
+ expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class)
+ expect(notes_holder_saved).to have_content test_note_comment
+
+ assert_form_is_reset if asset_form_reset
+ end
+
+ def assert_comment_dismissal(line_holder)
+ expect(line_holder).not_to have_xpath notes_holder_input_xpath
+ expect(page).not_to have_content test_note_comment
+
+ assert_form_is_reset
+ end
+
+ def assert_form_is_reset
+ expect(page).to have_no_css('.js-temp-notes-holder')
+ end
+end
diff --git a/spec/features/merge_requests/user_posts_notes.rb b/spec/features/merge_requests/user_posts_notes.rb
new file mode 100644
index 00000000000..c7cc4d6bc72
--- /dev/null
+++ b/spec/features/merge_requests/user_posts_notes.rb
@@ -0,0 +1,145 @@
+require 'spec_helper'
+
+describe 'Merge requests > User posts notes', :js do
+ let(:project) { create(:project) }
+ let(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
+ let!(:note) do
+ create(:note_on_merge_request, :with_attachment, noteable: merge_request,
+ project: project)
+ end
+
+ before do
+ login_as :admin
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ subject { page }
+
+ 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')
+ page.within('.js-main-target-form') do
+ expect(page).not_to have_link('Cancel')
+ end
+ end
+
+ describe 'with text' do
+ before do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: 'This is awesome'
+ end
+ end
+
+ it 'has enable submit button and preview button' do
+ page.within('.js-main-target-form') do
+ expect(page).not_to have_css('.js-comment-button[disabled]')
+ expect(page).to have_css('.js-md-preview-button', visible: true)
+ end
+ end
+ end
+ end
+
+ describe 'when posting a note' do
+ before do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: 'This is awesome!'
+ find('.js-md-preview-button').click
+ click_button 'Comment'
+ end
+ end
+
+ it 'is added and form reset' 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)
+ end
+ page.within('.js-main-target-form') do
+ is_expected.to have_css('.js-note-text', visible: true)
+ end
+ end
+ 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
+ find('.js-note-edit').click
+ end
+
+ it 'shows the note edit form and hide the note body' do
+ page.within("#note_#{note.id}") do
+ expect(find('.current-note-edit-form', visible: true)).to be_visible
+ expect(find('.note-edit-form', visible: true)).to be_visible
+ expect(find(:css, '.note-body > .note-text', visible: false)).not_to be_visible
+ end
+ end
+
+ it 'resets the edit note form textarea with the original content of the note if cancelled' 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
+ end
+
+ it 'allows using markdown buttons after saving a note and then trying to edit it again' do
+ page.within('.current-note-edit-form') do
+ fill_in 'note[note]', with: 'This is the new content'
+ find('.btn-save').click
+ end
+
+ find('.note').hover
+ find('.js-note-edit').click
+
+ page.within('.current-note-edit-form') do
+ expect(find('#note_note').value).to eq('This is the new content')
+ find('.js-md:first-child').click
+ expect(find('#note_note').value).to eq('This is the new content****')
+ end
+ end
+
+ it 'appends the edited at time to the note' do
+ page.within('.current-note-edit-form') do
+ fill_in 'note[note]', with: 'Some new content'
+ find('.btn-save').click
+ end
+
+ page.within("#note_#{note.id}") do
+ is_expected.to have_css('.note_edited_ago')
+ expect(find('.note_edited_ago').text).
+ to match(/less than a minute ago/)
+ end
+ end
+ end
+
+ describe 'deleting an attachment' do
+ before do
+ find('.note').hover
+ find('.js-note-edit').click
+ end
+
+ it '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
+ find('.js-note-attachment-delete').click
+ is_expected.not_to have_css('.note-attachment')
+ is_expected.not_to have_css('.current-note-edit-form')
+ wait_for_ajax
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/user_sees_system_notes_spec.rb b/spec/features/merge_requests/user_sees_system_notes_spec.rb
new file mode 100644
index 00000000000..55d0f9d728c
--- /dev/null
+++ b/spec/features/merge_requests/user_sees_system_notes_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+feature 'Merge requests > User sees system notes' do
+ let(:public_project) { create(:project, :public) }
+ let(:private_project) { create(:project, :private) }
+ let(:issue) { create(:issue, project: private_project) }
+ let(:merge_request) { create(:merge_request, source_project: public_project, source_branch: 'markdown') }
+ let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: public_project, note: "mentioned in #{issue.to_reference(public_project)}") }
+
+ context 'when logged-in as a member of the private project' do
+ before do
+ user = create(:user)
+ private_project.add_developer(user)
+ login_as(user)
+ end
+
+ it 'shows the system note' do
+ visit namespace_project_merge_request_path(public_project.namespace, public_project, merge_request)
+
+ expect(page).to have_css('.system-note')
+ end
+ end
+
+ context 'when not logged-in' do
+ it 'hides the system note' do
+ visit namespace_project_merge_request_path(public_project.namespace, public_project, merge_request)
+
+ expect(page).not_to have_css('.system-note')
+ end
+ end
+end
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 04e85ed3f73..7a2da623c58 100644
--- a/spec/features/merge_requests/merge_request_versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -36,8 +36,23 @@ feature 'Merge Request versions', js: true, feature: true do
expect(page).to have_content '5 changed files'
end
- it 'show the message about disabled comments' do
- expect(page).to have_content 'Comments are disabled'
+ it 'show the message about disabled comment creation' do
+ expect(page).to have_content 'comment creation is disabled'
+ end
+
+ it 'shows comments that were last relevant at that version' do
+ position = Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: nil,
+ new_line: 4,
+ diff_refs: merge_request_diff1.diff_refs
+ )
+ outdated_diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+ outdated_diff_note.position = outdated_diff_note.original_position
+ outdated_diff_note.save!
+
+ expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']")
end
end
@@ -92,14 +107,13 @@ feature 'Merge Request versions', js: true, feature: true do
it 'should have 0 chages between versions' do
page.within '.mr-version-compare-dropdown' do
- expect(page).to have_content 'version 1'
+ expect(find('.dropdown-toggle')).to have_content 'version 1'
end
page.within '.mr-version-dropdown' do
find('.btn-default').click
- find(:link, 'version 1').trigger('click')
+ click_link 'version 1'
end
-
expect(page).to have_content '0 changed files'
end
end
@@ -114,12 +128,12 @@ feature 'Merge Request versions', js: true, feature: true do
it 'should set the compared versions to be the same' do
page.within '.mr-version-compare-dropdown' do
- expect(page).to have_content 'version 2'
+ expect(find('.dropdown-toggle')).to have_content 'version 2'
end
page.within '.mr-version-dropdown' do
find('.btn-default').click
- find(:link, 'version 1').trigger('click')
+ click_link 'version 1'
end
page.within '.mr-version-compare-dropdown' do
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index a62c5435748..4e128cd4a7d 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -141,6 +141,27 @@ describe 'Merge request', :feature, :js do
end
end
+ context 'view merge request with MWPS enabled but automatically merge fails' do
+ before do
+ merge_request.update(
+ merge_when_pipeline_succeeds: true,
+ merge_user: merge_request.author,
+ merge_error: 'Something went wrong'
+ )
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'shows information about the merge error' do
+ # Wait for the `ci_status` and `merge_check` requests
+ wait_for_ajax
+
+ page.within('.mr-widget-body') do
+ expect(page).to have_content('Something went wrong')
+ end
+ end
+ end
+
context 'merge error' do
before do
allow_any_instance_of(Repository).to receive(:merge).and_return(false)
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
deleted file mode 100644
index 783f2e93909..00000000000
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ /dev/null
@@ -1,285 +0,0 @@
-require 'spec_helper'
-
-describe 'Comments', feature: true do
- include RepoHelpers
- include WaitForAjax
-
- describe 'On a merge request', js: true, feature: true do
- let!(:project) { create(:project) }
- let!(:merge_request) do
- create(:merge_request, source_project: project, target_project: project)
- end
-
- let!(:note) do
- create(:note_on_merge_request, :with_attachment, noteable: merge_request,
- project: project)
- end
-
- before do
- login_as :admin
- visit namespace_project_merge_request_path(project.namespace, project, merge_request)
- end
-
- subject { page }
-
- 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')
- page.within('.js-main-target-form') do
- expect(page).not_to have_link('Cancel')
- end
- end
-
- describe 'with text' do
- before do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: 'This is awesome'
- end
- end
-
- it 'has enable submit button and preview button' do
- page.within('.js-main-target-form') do
- expect(page).not_to have_css('.js-comment-button[disabled]')
- expect(page).to have_css('.js-md-preview-button', visible: true)
- end
- end
- end
- end
-
- describe 'when posting a note' do
- before do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: 'This is awsome!'
- find('.js-md-preview-button').click
- click_button 'Comment'
- end
- end
-
- it 'is added and form reset' do
- is_expected.to have_content('This is awsome!')
- 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)
- end
- page.within('.js-main-target-form') do
- is_expected.to have_css('.js-note-text', visible: true)
- end
- end
- end
-
- describe 'when editing a note', js: true 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
- find('.js-note-edit').click
- end
-
- it 'shows the note edit form and hide the note body' do
- page.within("#note_#{note.id}") do
- expect(find('.current-note-edit-form', visible: true)).to be_visible
- expect(find('.note-edit-form', visible: true)).to be_visible
- expect(find(:css, '.note-body > .note-text', visible: false)).not_to be_visible
- end
- end
-
- it 'resets the edit note form textarea with the original content of the note if cancelled' 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
- end
-
- it 'allows using markdown buttons after saving a note and then trying to edit it again' do
- page.within('.current-note-edit-form') do
- fill_in 'note[note]', with: 'This is the new content'
- find('.btn-save').click
- end
-
- find('.note').hover
- find('.js-note-edit').click
-
- page.within('.current-note-edit-form') do
- expect(find('#note_note').value).to eq('This is the new content')
- find('.js-md:first-child').click
- expect(find('#note_note').value).to eq('This is the new content****')
- end
- end
-
- it 'appends the edited at time to the note' do
- page.within('.current-note-edit-form') do
- fill_in 'note[note]', with: 'Some new content'
- find('.btn-save').click
- end
-
- page.within("#note_#{note.id}") do
- is_expected.to have_css('.note_edited_ago')
- expect(find('.note_edited_ago').text).
- to match(/less than a minute ago/)
- end
- end
- end
-
- describe 'deleting an attachment' do
- before do
- find('.note').hover
- find('.js-note-edit').click
- end
-
- it '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
- find('.js-note-attachment-delete').click
- is_expected.not_to have_css('.note-attachment')
- is_expected.not_to have_css('.current-note-edit-form')
- wait_for_ajax
- end
- end
- end
- end
-
- describe 'Handles cross-project system notes', js: true, feature: true do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:project2) { create(:project, :private) }
- let(:issue) { create(:issue, project: project2) }
- let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') }
- let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "mentioned in #{issue.to_reference(project)}") }
-
- it 'shows the system note' do
- login_as :admin
- visit namespace_project_merge_request_path(project.namespace, project, merge_request)
-
- expect(page).to have_css('.system-note')
- end
-
- it 'hides redacted system note' do
- visit namespace_project_merge_request_path(project.namespace, project, merge_request)
-
- expect(page).not_to have_css('.system-note')
- end
- end
-
- describe 'On a merge request diff', js: true, feature: true do
- let(:merge_request) { create(:merge_request) }
- let(:project) { merge_request.source_project }
-
- before do
- login_as :admin
- visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
- end
-
- subject { page }
-
- describe 'when adding a note' do
- before do
- click_diff_line
- end
-
- describe 'the notes holder' do
- it { is_expected.to have_css('.js-temp-notes-holder') }
-
- it 'has .new_note css class' do
- page.within('.js-temp-notes-holder') do
- expect(subject).to have_css('.new-note')
- end
- end
- end
-
- describe 'the note form' do
- it "does not add a second form for same row" do
- click_diff_line
-
- is_expected.
- to have_css("form[data-line-code='#{line_code}']",
- count: 1)
- end
-
- it 'is removed when canceled' do
- is_expected.to have_css('.js-temp-notes-holder')
-
- page.within("form[data-line-code='#{line_code}']") do
- find('.js-close-discussion-note-form').trigger('click')
- end
-
- is_expected.to have_no_css('.js-temp-notes-holder')
- end
- end
- end
-
- describe 'with muliple note forms' do
- before do
- click_diff_line
- click_diff_line(line_code_2)
- end
-
- it { is_expected.to have_css('.js-temp-notes-holder', count: 2) }
-
- describe 'previewing them separately' do
- before do
- # add two separate texts and trigger previews on both
- page.within("tr[id='#{line_code}'] + .js-temp-notes-holder") do
- fill_in 'note[note]', with: 'One comment on line 7'
- find('.js-md-preview-button').click
- end
- page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do
- fill_in 'note[note]', with: 'Another comment on line 10'
- find('.js-md-preview-button').click
- end
- end
- end
-
- describe 'posting a note' do
- before do
- page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do
- fill_in 'note[note]', with: 'Another comment on line 10'
- click_button('Comment')
- end
- end
-
- it 'adds as discussion' do
- is_expected.to have_content('Another comment on line 10')
- is_expected.to have_css('.notes_holder')
- is_expected.to have_css('.notes_holder .note', count: 1)
- is_expected.to have_button('Reply...')
- end
-
- it 'adds code to discussion' do
- click_button 'Reply...'
-
- page.within(first('.js-discussion-note-form')) do
- fill_in 'note[note]', with: '```{{ test }}```'
-
- click_button('Comment')
- end
-
- expect(page).to have_content('{{ test }}')
- end
- end
- end
- end
-
- def line_code
- sample_compare.changes.first[:line_code]
- end
-
- def line_code_2
- sample_compare.changes.last[:line_code]
- end
-
- def click_diff_line(data = line_code)
- find(".line_holder[id='#{data}'] td.line_content").hover
- find(".line_holder[id='#{data}'] button").trigger('click')
- end
-end
diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb
index fa1a753afcb..6ea149956fe 100644
--- a/spec/features/projects/blobs/user_create_spec.rb
+++ b/spec/features/projects/blobs/user_create_spec.rb
@@ -77,7 +77,7 @@ feature 'New blob creation', feature: true, js: true do
project,
user,
start_branch: 'master',
- target_branch: 'master',
+ branch_name: 'master',
commit_message: 'Create file',
file_path: 'feature.rb',
file_content: content
@@ -87,7 +87,7 @@ feature 'New blob creation', feature: true, js: true do
end
scenario 'shows error message' do
- expect(page).to have_content('Your changes could not be committed because a file with the same name already exists')
+ expect(page).to have_content('A file with this name already exists')
expect(page).to have_content('New file')
expect(page).to have_content('NextFeature')
end
diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb
index 5d7bd3dc4ce..de6905f2b58 100644
--- a/spec/features/projects/files/creating_a_file_spec.rb
+++ b/spec/features/projects/files/creating_a_file_spec.rb
@@ -29,16 +29,16 @@ feature 'User wants to create a file', feature: true do
scenario 'directory name contains Chinese characters' do
submit_new_file(file_name: '中文/测试.md')
- expect(page).to have_content 'The file has been successfully created.'
+ expect(page).to have_content 'The file has been successfully created'
end
scenario 'file name contains invalid characters' do
submit_new_file(file_name: '\\')
- expect(page).to have_content 'Your changes could not be committed, because the file name can contain only'
+ expect(page).to have_content 'Path can contain only'
end
scenario 'file name contains directory traversal' do
submit_new_file(file_name: '../README.md')
- expect(page).to have_content 'Your changes could not be committed, because the file name cannot include directory traversal.'
+ expect(page).to have_content 'Path cannot include directory traversal'
end
end
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
index 3e544316f28..4da34108b46 100644
--- a/spec/features/projects/files/editing_a_file_spec.rb
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -8,7 +8,7 @@ feature 'User wants to edit a file', feature: true do
let(:commit_params) do
{
start_branch: project.default_branch,
- target_branch: project.default_branch,
+ branch_name: project.default_branch,
commit_message: "Committing First Update",
file_path: ".gitignore",
file_content: "First Update",
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 62d0aedda48..6cdca0f114b 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -163,12 +163,14 @@ feature 'issuable templates', feature: true, js: true do
end
def select_template(name)
- first('.js-issuable-selector').click
- first('.js-issuable-selector-wrap .dropdown-content a', text: name).click
+ find('.js-issuable-selector').click
+
+ find('.js-issuable-selector-wrap .dropdown-content a', text: name, match: :first).click
end
def select_option(name)
- first('.js-issuable-selector').click
- first('.js-issuable-selector-wrap .dropdown-footer-list a', text: name).click
+ find('.js-issuable-selector').click
+
+ find('.js-issuable-selector-wrap .dropdown-footer-list a', text: name, match: :first).click
end
end
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
new file mode 100644
index 00000000000..deea34214fb
--- /dev/null
+++ b/spec/features/projects/members/list_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+feature 'Project members list', feature: true do
+ include Select2Helper
+
+ let(:user1) { create(:user, name: 'John Doe') }
+ let(:user2) { create(:user, name: 'Mary Jane') }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+
+ background do
+ login_as(user1)
+ group.add_owner(user1)
+ end
+
+ scenario 'show members from project and group' do
+ project.add_developer(user2)
+
+ visit_members_page
+
+ expect(first_row.text).to include(user1.name)
+ expect(second_row.text).to include(user2.name)
+ end
+
+ scenario 'show user once if member of both group and project' do
+ project.add_developer(user1)
+
+ visit_members_page
+
+ expect(first_row.text).to include(user1.name)
+ expect(second_row).to be_blank
+ end
+
+ scenario 'update user acess level', :js do
+ project.add_developer(user2)
+
+ visit_members_page
+
+ page.within(second_row) do
+ click_button('Developer')
+ click_link('Reporter')
+
+ expect(page).to have_button('Reporter')
+ end
+ end
+
+ scenario 'add user to project', :js do
+ visit_members_page
+
+ add_user(user2.id, 'Reporter')
+
+ page.within(second_row) do
+ expect(page).to have_content(user2.name)
+ expect(page).to have_button('Reporter')
+ end
+ end
+
+ scenario 'invite user to project', :js do
+ visit_members_page
+
+ add_user('test@example.com', 'Reporter')
+
+ page.within(second_row) do
+ expect(page).to have_content('test@example.com')
+ expect(page).to have_content('Invited')
+ expect(page).to have_button('Reporter')
+ end
+ end
+
+ def first_row
+ page.all('ul.content-list > li')[0]
+ end
+
+ def second_row
+ page.all('ul.content-list > li')[1]
+ end
+
+ def add_user(id, role)
+ page.within ".users-project-form" do
+ select2(id, from: "#user_ids", multiple: true)
+ select(role, from: "access_level")
+ end
+
+ click_button "Add to project"
+ end
+
+ def visit_members_page
+ visit namespace_project_settings_members_path(project.namespace, project)
+ end
+end
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index ce5c5f21167..34c6a10950f 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -25,7 +25,7 @@ describe 'View on environment', js: true do
project,
user,
start_branch: branch_name,
- target_branch: branch_name,
+ branch_name: branch_name,
commit_message: "Add .gitlab/route-map.yml",
file_path: '.gitlab/route-map.yml',
file_content: route_map
@@ -36,7 +36,7 @@ describe 'View on environment', js: true do
project,
user,
start_branch: branch_name,
- target_branch: branch_name,
+ branch_name: branch_name,
commit_message: "Update feature",
file_path: file_path,
file_content: "# Noop"
diff --git a/spec/features/projects/wiki/shortcuts_spec.rb b/spec/features/projects/wiki/shortcuts_spec.rb
new file mode 100644
index 00000000000..c1f6b0cce3b
--- /dev/null
+++ b/spec/features/projects/wiki/shortcuts_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+feature 'Wiki shortcuts', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
+ let(:wiki_page) do
+ WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
+ end
+
+ before do
+ login_as(user)
+ visit namespace_project_wiki_path(project.namespace, project, wiki_page)
+ end
+
+ scenario 'Visit edit wiki page using "e" keyboard shortcut' do
+ find('body').native.send_key('e')
+
+ expect(find('.wiki-page-title')).to have_content('Edit Page')
+ end
+end
diff --git a/spec/fixtures/trace/ansi-sequence-and-unicode b/spec/fixtures/trace/ansi-sequence-and-unicode
new file mode 100644
index 00000000000..5d2466f0d0f
--- /dev/null
+++ b/spec/fixtures/trace/ansi-sequence-and-unicode
@@ -0,0 +1,5 @@
+.
+..
+😺
+ヾ(´༎ຶД༎ຶ`)ノ
+許功蓋
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index f3e79cc7290..2c0e9975f73 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -86,10 +86,10 @@ describe PreferencesHelper do
context 'when repository is not empty' do
let(:project) { create(:project, :public, :repository) }
- it 'returns readme if user has repository access' do
+ it 'returns files and readme if user has repository access' do
allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(true)
- expect(helper.default_project_view).to eq('readme')
+ expect(helper.default_project_view).to eq('files')
end
it 'returns activity if user does not have repository access' do
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 40efab6e4f7..a7fc5d14859 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -265,4 +265,27 @@ describe ProjectsHelper do
end
end
end
+
+ describe "#visibility_select_options" do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ end
+
+ it "does not include the Public restricted level" do
+ expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).not_to include('Public')
+ end
+
+ it "includes the Internal level" do
+ expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Internal')
+ end
+
+ it "includes the Private level" do
+ expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Private')
+ end
+ end
end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 28b8def331d..345bc33a67b 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -70,10 +70,12 @@ describe SubmoduleHelper do
expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
end
- it 'returns original with non-standard url' do
+ it 'handles urls with no .git on the end' do
stub_url('http://github.com/gitlab-org/gitlab-ce')
- expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
+ expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
+ end
+ it 'returns original with non-standard url' do
stub_url('http://github.com/another/gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
end
@@ -95,10 +97,12 @@ describe SubmoduleHelper do
expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
- it 'returns original with non-standard url' do
+ it 'handles urls with no .git on the end' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce')
- expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
+ expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
+ end
+ it 'returns original with non-standard url' do
stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index ea7753c7a1d..68ad5f66676 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -3,6 +3,8 @@
import Cookies from 'js-cookie';
import AwardsHandler from '~/awards_handler';
+require('~/lib/utils/common_utils');
+
(function() {
var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu;
@@ -28,7 +30,7 @@ import AwardsHandler from '~/awards_handler';
loadFixtures('issues/issue_with_comment.html.raw');
awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
- return function(url, emoji, cb) {
+ return function(button, url, emoji, cb) {
return cb();
};
})(this));
@@ -63,7 +65,7 @@ import AwardsHandler from '~/awards_handler';
$emojiMenu = $('.emoji-menu');
expect($emojiMenu.length).toBe(1);
expect($emojiMenu.hasClass('is-visible')).toBe(true);
- expect($emojiMenu.find('#emoji_search').length).toBe(1);
+ expect($emojiMenu.find('.js-emoji-menu-search').length).toBe(1);
return expect($('.js-awards-block.current').length).toBe(1);
});
});
@@ -115,6 +117,27 @@ import AwardsHandler from '~/awards_handler';
return expect($emojiButton.next('.js-counter').text()).toBe('4');
});
});
+ describe('::userAuthored', function() {
+ it('should update tooltip to user authored title', function() {
+ var $thumbsUpEmoji, $votesBlock;
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'sam');
+ awardsHandler.userAuthored($thumbsUpEmoji);
+ return expect($thumbsUpEmoji.data("original-title")).toBe("You cannot vote on your own issue, MR and note");
+ });
+ it('should restore tooltip back to initial vote list', function() {
+ var $thumbsUpEmoji, $votesBlock;
+ jasmine.clock().install();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'sam');
+ awardsHandler.userAuthored($thumbsUpEmoji);
+ jasmine.clock().tick(2801);
+ jasmine.clock().uninstall();
+ return expect($thumbsUpEmoji.data("original-title")).toBe("sam");
+ });
+ });
describe('::getAwardUrl', function() {
return it('returns the url for request', function() {
return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji');
@@ -194,16 +217,35 @@ import AwardsHandler from '~/awards_handler';
return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
});
});
- describe('search', function() {
- return it('should filter the emoji', function(done) {
+ describe('::searchEmojis', () => {
+ it('should filter the emoji', function(done) {
return openAndWaitForEmojiMenu()
.then(() => {
expect($('[data-name=angel]').is(':visible')).toBe(true);
expect($('[data-name=anger]').is(':visible')).toBe(true);
- $('#emoji_search').val('ali').trigger('input');
+ awardsHandler.searchEmojis('ali');
expect($('[data-name=angel]').is(':visible')).toBe(false);
expect($('[data-name=anger]').is(':visible')).toBe(false);
expect($('[data-name=alien]').is(':visible')).toBe(true);
+ expect($('.js-emoji-menu-search').val()).toBe('ali');
+ })
+ .then(done)
+ .catch((err) => {
+ done.fail(`Failed to open and build emoji menu: ${err.message}`);
+ });
+ });
+ it('should clear the search when searching for nothing', function(done) {
+ return openAndWaitForEmojiMenu()
+ .then(() => {
+ awardsHandler.searchEmojis('ali');
+ expect($('[data-name=angel]').is(':visible')).toBe(false);
+ expect($('[data-name=anger]').is(':visible')).toBe(false);
+ expect($('[data-name=alien]').is(':visible')).toBe(true);
+ awardsHandler.searchEmojis('');
+ expect($('[data-name=angel]').is(':visible')).toBe(true);
+ expect($('[data-name=anger]').is(':visible')).toBe(true);
+ expect($('[data-name=alien]').is(':visible')).toBe(true);
+ expect($('.js-emoji-menu-search').val()).toBe('');
})
.then(done)
.catch((err) => {
@@ -211,6 +253,7 @@ import AwardsHandler from '~/awards_handler';
});
});
});
+
describe('emoji menu', function() {
const emojiSelector = '[data-name="sunglasses"]';
const openEmojiMenuAndAddEmoji = function() {
diff --git a/spec/javascripts/blob/blob_fork_suggestion_spec.js b/spec/javascripts/blob/blob_fork_suggestion_spec.js
new file mode 100644
index 00000000000..d0d64d75957
--- /dev/null
+++ b/spec/javascripts/blob/blob_fork_suggestion_spec.js
@@ -0,0 +1,37 @@
+import BlobForkSuggestion from '~/blob/blob_fork_suggestion';
+
+describe('BlobForkSuggestion', () => {
+ let blobForkSuggestion;
+
+ const openButtons = [document.createElement('div')];
+ const forkButtons = [document.createElement('a')];
+ const cancelButtons = [document.createElement('div')];
+ const suggestionSections = [document.createElement('div')];
+ const actionTextPieces = [document.createElement('div')];
+
+ beforeEach(() => {
+ blobForkSuggestion = new BlobForkSuggestion({
+ openButtons,
+ forkButtons,
+ cancelButtons,
+ suggestionSections,
+ actionTextPieces,
+ });
+ });
+
+ afterEach(() => {
+ blobForkSuggestion.destroy();
+ });
+
+ it('showSuggestionSection', () => {
+ blobForkSuggestion.showSuggestionSection('/foo', 'foo');
+ expect(suggestionSections[0].classList.contains('hidden')).toEqual(false);
+ expect(forkButtons[0].getAttribute('href')).toEqual('/foo');
+ expect(actionTextPieces[0].textContent).toEqual('foo');
+ });
+
+ it('hideSuggestionSection', () => {
+ blobForkSuggestion.hideSuggestionSection();
+ expect(suggestionSections[0].classList.contains('hidden')).toEqual(true);
+ });
+});
diff --git a/spec/javascripts/blob/sketch/index_spec.js b/spec/javascripts/blob/sketch/index_spec.js
index 0e4431548c4..79f40559817 100644
--- a/spec/javascripts/blob/sketch/index_spec.js
+++ b/spec/javascripts/blob/sketch/index_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-new */
+/* eslint-disable no-new, promise/catch-or-return */
import JSZip from 'jszip';
import SketchLoader from '~/blob/sketch';
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index a9d4c6ef76f..24a2da9f6b6 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -107,4 +107,44 @@ describe('List model', () => {
expect(gl.boardService.moveIssue)
.toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined);
});
+
+ describe('page number', () => {
+ beforeEach(() => {
+ spyOn(list, 'getIssues');
+ });
+
+ it('increase page number if current issue count is more than the page size', () => {
+ for (let i = 0; i < 30; i += 1) {
+ list.issues.push(new ListIssue({
+ title: 'Testing',
+ iid: _.random(10000) + i,
+ confidential: false,
+ labels: [list.label]
+ }));
+ }
+ list.issuesSize = 50;
+
+ expect(list.issues.length).toBe(30);
+
+ list.nextPage();
+
+ expect(list.page).toBe(2);
+ expect(list.getIssues).toHaveBeenCalled();
+ });
+
+ it('does not increase page number if issue count is less than the page size', () => {
+ list.issues.push(new ListIssue({
+ title: 'Testing',
+ iid: _.random(10000),
+ confidential: false,
+ labels: [list.label]
+ }));
+ list.issuesSize = 2;
+
+ list.nextPage();
+
+ expect(list.page).toBe(1);
+ expect(list.getIssues).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
index edd4b3c1440..8ec96bdb583 100644
--- a/spec/javascripts/build_spec.js
+++ b/spec/javascripts/build_spec.js
@@ -1,11 +1,11 @@
/* eslint-disable no-new */
/* global Build */
-
-require('~/lib/utils/datetime_utility');
-require('~/lib/utils/url_utility');
-require('~/build');
-require('~/breakpoints');
-require('vendor/jquery.nicescroll');
+import { bytesToKiB } from '~/lib/utils/number_utils';
+import '~/lib/utils/datetime_utility';
+import '~/lib/utils/url_utility';
+import '~/build';
+import '~/breakpoints';
+import 'vendor/jquery.nicescroll';
describe('Build', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
@@ -98,9 +98,9 @@ describe('Build', () => {
jasmine.clock().tick(4001);
- expect($.ajax.calls.count()).toBe(2);
+ expect($.ajax.calls.count()).toBe(3);
- args = $.ajax.calls.argsFor(1)[0];
+ args = $.ajax.calls.argsFor(2)[0];
expect(args.url).toBe(`${BUILD_URL}/trace.json`);
expect(args.dataType).toBe('json');
expect(args.data.state).toBe('newstate');
@@ -133,7 +133,7 @@ describe('Build', () => {
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
jasmine.clock().tick(4001);
- args = $.ajax.calls.argsFor(1)[0];
+ args = $.ajax.calls.argsFor(2)[0];
args.success.call($, {
html: '<span>Different</span>',
status: 'running',
@@ -144,24 +144,6 @@ describe('Build', () => {
expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
});
- it('shows information about truncated log', () => {
- jasmine.clock().tick(4001);
- const [{ success }] = $.ajax.calls.argsFor(0);
-
- success.call($, {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- truncated: true,
- size: '50',
- });
-
- expect(
- $('#build-trace .js-truncated-info').text().trim(),
- ).toContain('Showing last 50 KiB of log');
- expect($('#build-trace .js-truncated-info-size').text()).toMatch('50');
- });
-
it('reloads the page when the build is done', () => {
spyOn(gl.utils, 'visitUrl');
@@ -176,6 +158,107 @@ describe('Build', () => {
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
});
+
+ describe('truncated information', () => {
+ describe('when size is less than total', () => {
+ it('shows information about truncated log', () => {
+ jasmine.clock().tick(4001);
+ const [{ success }] = $.ajax.calls.argsFor(0);
+
+ success.call($, {
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 50,
+ total: 100,
+ });
+
+ expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
+ });
+
+ it('shows the size in KiB', () => {
+ jasmine.clock().tick(4001);
+ const [{ success }] = $.ajax.calls.argsFor(0);
+ const size = 50;
+
+ success.call($, {
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size,
+ total: 100,
+ });
+
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${bytesToKiB(size)}`);
+ });
+
+ it('shows incremented size', () => {
+ jasmine.clock().tick(4001);
+ let args = $.ajax.calls.argsFor(0)[0];
+ args.success.call($, {
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 50,
+ total: 100,
+ });
+
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${bytesToKiB(50)}`);
+
+ jasmine.clock().tick(4001);
+ args = $.ajax.calls.argsFor(2)[0];
+ args.success.call($, {
+ html: '<span>Update</span>',
+ status: 'success',
+ append: true,
+ size: 10,
+ total: 100,
+ });
+
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${bytesToKiB(60)}`);
+ });
+
+ it('renders the raw link', () => {
+ jasmine.clock().tick(4001);
+ const [{ success }] = $.ajax.calls.argsFor(0);
+
+ success.call($, {
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 50,
+ total: 100,
+ });
+
+ expect(
+ document.querySelector('.js-raw-link').textContent.trim(),
+ ).toContain('Complete Raw');
+ });
+ });
+
+ describe('when size is equal than total', () => {
+ it('does not show the trunctated information', () => {
+ jasmine.clock().tick(4001);
+ const [{ success }] = $.ajax.calls.argsFor(0);
+
+ success.call($, {
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 100,
+ total: 100,
+ });
+
+ expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
+ });
+ });
+ });
});
});
});
diff --git a/spec/javascripts/ci_status_icon_spec.js b/spec/javascripts/ci_status_icon_spec.js
new file mode 100644
index 00000000000..c83416c15ef
--- /dev/null
+++ b/spec/javascripts/ci_status_icon_spec.js
@@ -0,0 +1,44 @@
+import * as icons from '~/ci_status_icons';
+
+describe('CI status icons', () => {
+ const statuses = [
+ 'canceled',
+ 'created',
+ 'failed',
+ 'manual',
+ 'pending',
+ 'running',
+ 'skipped',
+ 'success',
+ 'warning',
+ ];
+
+ statuses.forEach((status) => {
+ it(`should export a ${status} svg`, () => {
+ const key = `${status.toUpperCase()}_SVG`;
+
+ expect(Object.hasOwnProperty.call(icons, key)).toBe(true);
+ expect(icons[key]).toMatch(/^<svg/);
+ });
+ });
+
+ describe('default export map', () => {
+ const entityIconNames = [
+ 'icon_status_canceled',
+ 'icon_status_created',
+ 'icon_status_failed',
+ 'icon_status_manual',
+ 'icon_status_pending',
+ 'icon_status_running',
+ 'icon_status_skipped',
+ 'icon_status_success',
+ 'icon_status_warning',
+ ];
+
+ entityIconNames.forEach((iconName) => {
+ it(`should have a '${iconName}' key`, () => {
+ expect(Object.hasOwnProperty.call(icons.default, iconName)).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 8cac3cad232..ad31448f81c 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -36,6 +36,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
setTimeout(() => {
expect(this.component.$el.querySelector('.empty-state')).toBeDefined();
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
+ expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null);
done();
}, 1);
});
@@ -67,6 +68,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
setTimeout(() => {
expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
+ expect(this.component.$el.querySelector('.empty-state')).toBe(null);
+ expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null);
done();
}, 0);
});
@@ -95,10 +98,12 @@ describe('Pipelines table in Commits and Merge requests', () => {
this.component.$destroy();
});
- it('should render empty state', function (done) {
+ it('should render error state', function (done) {
setTimeout(() => {
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
+ expect(this.component.$el.querySelector('.js-empty-state')).toBe(null);
+ expect(this.component.$el.querySelector('table')).toBe(null);
done();
}, 0);
});
diff --git a/spec/javascripts/diff_comments_store_spec.js b/spec/javascripts/diff_comments_store_spec.js
index 84cf98c930a..66ece7e4f41 100644
--- a/spec/javascripts/diff_comments_store_spec.js
+++ b/spec/javascripts/diff_comments_store_spec.js
@@ -5,129 +5,127 @@ require('~/diff_notes/models/discussion');
require('~/diff_notes/models/note');
require('~/diff_notes/stores/comments');
-(() => {
- function createDiscussion(noteId = 1, resolved = true) {
- CommentsStore.create({
- discussionId: 'a',
- noteId,
- canResolve: true,
- resolved,
- resolvedBy: 'test',
- authorName: 'test',
- authorAvatar: 'test',
- noteTruncated: 'test...',
- });
- }
-
- beforeEach(() => {
- CommentsStore.state = {};
+function createDiscussion(noteId = 1, resolved = true) {
+ CommentsStore.create({
+ discussionId: 'a',
+ noteId,
+ canResolve: true,
+ resolved,
+ resolvedBy: 'test',
+ authorName: 'test',
+ authorAvatar: 'test',
+ noteTruncated: 'test...',
});
+}
- describe('New discussion', () => {
- it('creates new discussion', () => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- expect(Object.keys(CommentsStore.state).length).toBe(1);
- });
+beforeEach(() => {
+ CommentsStore.state = {};
+});
- it('creates new note in discussion', () => {
- createDiscussion();
- createDiscussion(2);
+describe('New discussion', () => {
+ it('creates new discussion', () => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ expect(Object.keys(CommentsStore.state).length).toBe(1);
+ });
- const discussion = CommentsStore.state['a'];
- expect(Object.keys(discussion.notes).length).toBe(2);
- });
+ it('creates new note in discussion', () => {
+ createDiscussion();
+ createDiscussion(2);
+
+ const discussion = CommentsStore.state['a'];
+ expect(Object.keys(discussion.notes).length).toBe(2);
});
+});
- describe('Get note', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
+describe('Get note', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
- it('gets note by ID', () => {
- const note = CommentsStore.get('a', 1);
- expect(note).toBeDefined();
- expect(note.id).toBe(1);
- });
+ it('gets note by ID', () => {
+ const note = CommentsStore.get('a', 1);
+ expect(note).toBeDefined();
+ expect(note.id).toBe(1);
});
+});
- describe('Delete discussion', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
+describe('Delete discussion', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
- it('deletes discussion by ID', () => {
- CommentsStore.delete('a', 1);
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- });
+ it('deletes discussion by ID', () => {
+ CommentsStore.delete('a', 1);
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ });
- it('deletes discussion when no more notes', () => {
- createDiscussion();
- createDiscussion(2);
- expect(Object.keys(CommentsStore.state).length).toBe(1);
- expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2);
+ it('deletes discussion when no more notes', () => {
+ createDiscussion();
+ createDiscussion(2);
+ expect(Object.keys(CommentsStore.state).length).toBe(1);
+ expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2);
- CommentsStore.delete('a', 1);
- CommentsStore.delete('a', 2);
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- });
+ CommentsStore.delete('a', 1);
+ CommentsStore.delete('a', 2);
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
});
+});
- describe('Update note', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
+describe('Update note', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
- it('updates note to be unresolved', () => {
- CommentsStore.update('a', 1, false, 'test');
+ it('updates note to be unresolved', () => {
+ CommentsStore.update('a', 1, false, 'test');
- const note = CommentsStore.get('a', 1);
- expect(note.resolved).toBe(false);
- });
+ const note = CommentsStore.get('a', 1);
+ expect(note.resolved).toBe(false);
});
+});
- describe('Discussion resolved', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
+describe('Discussion resolved', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
- it('is resolved with single note', () => {
- const discussion = CommentsStore.state['a'];
- expect(discussion.isResolved()).toBe(true);
- });
+ it('is resolved with single note', () => {
+ const discussion = CommentsStore.state['a'];
+ expect(discussion.isResolved()).toBe(true);
+ });
- it('is unresolved with 2 notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2, false);
+ it('is unresolved with 2 notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2, false);
- expect(discussion.isResolved()).toBe(false);
- });
+ expect(discussion.isResolved()).toBe(false);
+ });
- it('is resolved with 2 notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2);
+ it('is resolved with 2 notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2);
- expect(discussion.isResolved()).toBe(true);
- });
+ expect(discussion.isResolved()).toBe(true);
+ });
- it('resolve all notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2, false);
+ it('resolve all notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2, false);
- discussion.resolveAllNotes();
- expect(discussion.isResolved()).toBe(true);
- });
+ discussion.resolveAllNotes();
+ expect(discussion.isResolved()).toBe(true);
+ });
- it('unresolve all notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2);
+ it('unresolve all notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2);
- discussion.unResolveAllNotes();
- expect(discussion.isResolved()).toBe(false);
- });
+ discussion.unResolveAllNotes();
+ expect(discussion.isResolved()).toBe(false);
});
-})();
+});
diff --git a/spec/javascripts/droplab/constants_spec.js b/spec/javascripts/droplab/constants_spec.js
index 35239e4fb8e..fd153a49fcd 100644
--- a/spec/javascripts/droplab/constants_spec.js
+++ b/spec/javascripts/droplab/constants_spec.js
@@ -26,4 +26,10 @@ describe('constants', function () {
expect(constants.ACTIVE_CLASS).toBe('droplab-item-active');
});
});
+
+ describe('IGNORE_CLASS', function () {
+ it('should be `droplab-item-ignore`', function() {
+ expect(constants.IGNORE_CLASS).toBe('droplab-item-ignore');
+ });
+ });
});
diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js
index 802e2435672..7516b301917 100644
--- a/spec/javascripts/droplab/drop_down_spec.js
+++ b/spec/javascripts/droplab/drop_down_spec.js
@@ -2,7 +2,7 @@
import DropDown from '~/droplab/drop_down';
import utils from '~/droplab/utils';
-import { SELECTED_CLASS } from '~/droplab/constants';
+import { SELECTED_CLASS, IGNORE_CLASS } from '~/droplab/constants';
describe('DropDown', function () {
describe('class constructor', function () {
@@ -128,9 +128,10 @@ describe('DropDown', function () {
describe('clickEvent', function () {
beforeEach(function () {
+ this.classList = jasmine.createSpyObj('classList', ['contains']);
this.list = { dispatchEvent: () => {} };
this.dropdown = { hide: () => {}, list: this.list, addSelectedClass: () => {} };
- this.event = { preventDefault: () => {}, target: {} };
+ this.event = { preventDefault: () => {}, target: { classList: this.classList } };
this.customEvent = {};
this.closestElement = {};
@@ -140,6 +141,7 @@ describe('DropDown', function () {
spyOn(this.event, 'preventDefault');
spyOn(window, 'CustomEvent').and.returnValue(this.customEvent);
spyOn(utils, 'closest').and.returnValues(this.closestElement, undefined);
+ this.classList.contains.and.returnValue(false);
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
});
@@ -164,15 +166,35 @@ describe('DropDown', function () {
expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object));
});
+ it('should call .classList.contains checking for IGNORE_CLASS', function () {
+ expect(this.classList.contains).toHaveBeenCalledWith(IGNORE_CLASS);
+ });
+
it('should call .dispatchEvent with the customEvent', function () {
expect(this.list.dispatchEvent).toHaveBeenCalledWith(this.customEvent);
});
describe('if the target is a UL element', function () {
beforeEach(function () {
- this.event = { preventDefault: () => {}, target: { tagName: 'UL' } };
+ this.event = { preventDefault: () => {}, target: { tagName: 'UL', classList: this.classList } };
+
+ spyOn(this.event, 'preventDefault');
+ utils.closest.calls.reset();
+
+ DropDown.prototype.clickEvent.call(this.dropdown, this.event);
+ });
+
+ it('should return immediately', function () {
+ expect(utils.closest).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if the target has the IGNORE_CLASS class', function () {
+ beforeEach(function () {
+ this.event = { preventDefault: () => {}, target: { tagName: 'LI', classList: this.classList } };
spyOn(this.event, 'preventDefault');
+ this.classList.contains.and.returnValue(true);
utils.closest.calls.reset();
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js
index 6348d97b0a5..676bf61cfd9 100644
--- a/spec/javascripts/environments/environment_actions_spec.js
+++ b/spec/javascripts/environments/environment_actions_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import actionsComp from '~/environments/components/environment_actions';
+import actionsComp from '~/environments/components/environment_actions.vue';
describe('Actions Component', () => {
let ActionsComponent;
diff --git a/spec/javascripts/environments/environment_external_url_spec.js b/spec/javascripts/environments/environment_external_url_spec.js
index 9af218a27ff..056d68a26e9 100644
--- a/spec/javascripts/environments/environment_external_url_spec.js
+++ b/spec/javascripts/environments/environment_external_url_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import externalUrlComp from '~/environments/components/environment_external_url';
+import externalUrlComp from '~/environments/components/environment_external_url.vue';
describe('External URL Component', () => {
let ExternalUrlComponent;
diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js
index 4d42de4d549..0e141adb628 100644
--- a/spec/javascripts/environments/environment_item_spec.js
+++ b/spec/javascripts/environments/environment_item_spec.js
@@ -1,6 +1,6 @@
import 'timeago.js';
import Vue from 'vue';
-import environmentItemComp from '~/environments/components/environment_item';
+import environmentItemComp from '~/environments/components/environment_item.vue';
describe('Environment item', () => {
let EnvironmentItem;
diff --git a/spec/javascripts/environments/environment_monitoring_spec.js b/spec/javascripts/environments/environment_monitoring_spec.js
index fc451cce641..0f3dba66230 100644
--- a/spec/javascripts/environments/environment_monitoring_spec.js
+++ b/spec/javascripts/environments/environment_monitoring_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import monitoringComp from '~/environments/components/environment_monitoring';
+import monitoringComp from '~/environments/components/environment_monitoring.vue';
describe('Monitoring Component', () => {
let MonitoringComponent;
diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js
index 7cb39d9df03..25397714a76 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js
+++ b/spec/javascripts/environments/environment_rollback_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import rollbackComp from '~/environments/components/environment_rollback';
+import rollbackComp from '~/environments/components/environment_rollback.vue';
describe('Rollback Component', () => {
const retryURL = 'https://gitlab.com/retry';
diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js
index 4431baa4b96..9762688af1a 100644
--- a/spec/javascripts/environments/environment_spec.js
+++ b/spec/javascripts/environments/environment_spec.js
@@ -83,9 +83,10 @@ describe('Environment', () => {
it('should render a table with environments', (done) => {
setTimeout(() => {
+ expect(component.$el.querySelectorAll('table')).toBeDefined();
expect(
- component.$el.querySelectorAll('table tbody tr').length,
- ).toEqual(1);
+ component.$el.querySelector('.environment-name').textContent.trim(),
+ ).toEqual(environment.name);
done();
}, 0);
});
diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js
index 01055e3f255..942e4aaabd4 100644
--- a/spec/javascripts/environments/environment_stop_spec.js
+++ b/spec/javascripts/environments/environment_stop_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import stopComp from '~/environments/components/environment_stop';
+import stopComp from '~/environments/components/environment_stop.vue';
describe('Stop Component', () => {
let StopComponent;
diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js
index 3df967848a7..effbc6c3ee1 100644
--- a/spec/javascripts/environments/environment_table_spec.js
+++ b/spec/javascripts/environments/environment_table_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import environmentTableComp from '~/environments/components/environments_table';
+import environmentTableComp from '~/environments/components/environments_table.vue';
describe('Environment item', () => {
preloadFixtures('static/environments/element.html.raw');
diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js
index be2289edc2b..858472af4b6 100644
--- a/spec/javascripts/environments/environment_terminal_button_spec.js
+++ b/spec/javascripts/environments/environment_terminal_button_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import terminalComp from '~/environments/components/environment_terminal_button';
+import terminalComp from '~/environments/components/environment_terminal_button.vue';
describe('Stop Component', () => {
let TerminalComponent;
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index 43a217a67f5..72f3db29a66 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -47,9 +47,10 @@ describe('Environments Folder View', () => {
it('should render a table with environments', (done) => {
setTimeout(() => {
+ expect(component.$el.querySelectorAll('table')).toBeDefined();
expect(
- component.$el.querySelectorAll('table tbody tr').length,
- ).toEqual(2);
+ component.$el.querySelector('.environment-name').textContent.trim(),
+ ).toEqual(environmentsList[0].name);
done();
}, 0);
});
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js
index 2b1fe5e3eef..3f92fe4701e 100644
--- a/spec/javascripts/filtered_search/dropdown_user_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js
@@ -3,69 +3,67 @@ require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown');
require('~/filtered_search/dropdown_user');
-(() => {
- describe('Dropdown User', () => {
- describe('getSearchInput', () => {
- let dropdownUser;
+describe('Dropdown User', () => {
+ describe('getSearchInput', () => {
+ let dropdownUser;
- beforeEach(() => {
- spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
- spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
- spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
+ beforeEach(() => {
+ spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
- dropdownUser = new gl.DropdownUser();
- });
-
- it('should not return the double quote found in value', () => {
- spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
- lastToken: '"johnny appleseed',
- });
+ dropdownUser = new gl.DropdownUser();
+ });
- expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
+ it('should not return the double quote found in value', () => {
+ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
+ lastToken: '"johnny appleseed',
});
- it('should not return the single quote found in value', () => {
- spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
- lastToken: '\'larry boy',
- });
+ expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
+ });
- expect(dropdownUser.getSearchInput()).toBe('larry boy');
+ it('should not return the single quote found in value', () => {
+ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
+ lastToken: '\'larry boy',
});
+
+ expect(dropdownUser.getSearchInput()).toBe('larry boy');
});
+ });
- describe('config AjaxFilter\'s endpoint', () => {
- beforeEach(() => {
- spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
- spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
- });
+ describe('config AjaxFilter\'s endpoint', () => {
+ beforeEach(() => {
+ spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ });
- it('should return endpoint', () => {
- window.gon = {
- relative_url_root: '',
- };
- const dropdown = new gl.DropdownUser();
+ it('should return endpoint', () => {
+ window.gon = {
+ relative_url_root: '',
+ };
+ const dropdown = new gl.DropdownUser();
- expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json');
- });
+ expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json');
+ });
- it('should return endpoint when relative_url_root is undefined', () => {
- const dropdown = new gl.DropdownUser();
+ it('should return endpoint when relative_url_root is undefined', () => {
+ const dropdown = new gl.DropdownUser();
- expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json');
- });
+ expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json');
+ });
- it('should return endpoint with relative url when available', () => {
- window.gon = {
- relative_url_root: '/gitlab_directory',
- };
- const dropdown = new gl.DropdownUser();
+ it('should return endpoint with relative url when available', () => {
+ window.gon = {
+ relative_url_root: '/gitlab_directory',
+ };
+ const dropdown = new gl.DropdownUser();
- expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json');
- });
+ expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json');
+ });
- afterEach(() => {
- window.gon = {};
- });
+ afterEach(() => {
+ window.gon = {};
});
});
-})();
+});
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js
index e6538020896..c820c955172 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -3,308 +3,306 @@ require('~/filtered_search/dropdown_utils');
require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager');
-(() => {
- describe('Dropdown Utils', () => {
- describe('getEscapedText', () => {
- it('should return same word when it has no space', () => {
- const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
- expect(escaped).toBe('textWithoutSpace');
- });
+describe('Dropdown Utils', () => {
+ describe('getEscapedText', () => {
+ it('should return same word when it has no space', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
+ expect(escaped).toBe('textWithoutSpace');
+ });
- it('should escape with double quotes', () => {
- let escaped = gl.DropdownUtils.getEscapedText('text with space');
- expect(escaped).toBe('"text with space"');
+ it('should escape with double quotes', () => {
+ let escaped = gl.DropdownUtils.getEscapedText('text with space');
+ expect(escaped).toBe('"text with space"');
- escaped = gl.DropdownUtils.getEscapedText('won\'t fix');
- expect(escaped).toBe('"won\'t fix"');
- });
+ escaped = gl.DropdownUtils.getEscapedText('won\'t fix');
+ expect(escaped).toBe('"won\'t fix"');
+ });
- it('should escape with single quotes', () => {
- const escaped = gl.DropdownUtils.getEscapedText('won"t fix');
- expect(escaped).toBe('\'won"t fix\'');
- });
+ it('should escape with single quotes', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('won"t fix');
+ expect(escaped).toBe('\'won"t fix\'');
+ });
- it('should escape with single quotes by default', () => {
- const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix');
- expect(escaped).toBe('\'won"t\' fix\'');
- });
+ it('should escape with single quotes by default', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix');
+ expect(escaped).toBe('\'won"t\' fix\'');
});
+ });
- describe('filterWithSymbol', () => {
- let input;
- const item = {
- title: '@root',
- };
+ describe('filterWithSymbol', () => {
+ let input;
+ const item = {
+ title: '@root',
+ };
- beforeEach(() => {
- setFixtures(`
- <input type="text" id="test" />
- `);
+ beforeEach(() => {
+ setFixtures(`
+ <input type="text" id="test" />
+ `);
- input = document.getElementById('test');
- });
+ input = document.getElementById('test');
+ });
- it('should filter without symbol', () => {
- input.value = 'roo';
+ it('should filter without symbol', () => {
+ input.value = 'roo';
- const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
- it('should filter with symbol', () => {
- input.value = '@roo';
+ it('should filter with symbol', () => {
+ input.value = '@roo';
- const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
- describe('filters multiple word title', () => {
- const multipleWordItem = {
- title: 'Community Contributions',
- };
+ describe('filters multiple word title', () => {
+ const multipleWordItem = {
+ title: 'Community Contributions',
+ };
- it('should filter with double quote', () => {
- input.value = '"';
+ it('should filter with double quote', () => {
+ input.value = '"';
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
- it('should filter with double quote and symbol', () => {
- input.value = '~"';
+ it('should filter with double quote and symbol', () => {
+ input.value = '~"';
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
- it('should filter with double quote and multiple words', () => {
- input.value = '"community con';
+ it('should filter with double quote and multiple words', () => {
+ input.value = '"community con';
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
- it('should filter with double quote, symbol and multiple words', () => {
- input.value = '~"community con';
+ it('should filter with double quote, symbol and multiple words', () => {
+ input.value = '~"community con';
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
- it('should filter with single quote', () => {
- input.value = '\'';
+ it('should filter with single quote', () => {
+ input.value = '\'';
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
- it('should filter with single quote and symbol', () => {
- input.value = '~\'';
+ it('should filter with single quote and symbol', () => {
+ input.value = '~\'';
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
- it('should filter with single quote and multiple words', () => {
- input.value = '\'community con';
+ it('should filter with single quote and multiple words', () => {
+ input.value = '\'community con';
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
- it('should filter with single quote, symbol and multiple words', () => {
- input.value = '~\'community con';
+ it('should filter with single quote, symbol and multiple words', () => {
+ input.value = '~\'community con';
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
});
});
+ });
- describe('filterHint', () => {
- let input;
-
- beforeEach(() => {
- setFixtures(`
- <ul class="tokens-container">
- <li class="input-token">
- <input class="filtered-search" type="text" id="test" />
- </li>
- </ul>
- `);
-
- input = document.getElementById('test');
- });
+ describe('filterHint', () => {
+ let input;
- it('should filter', () => {
- input.value = 'l';
- let updatedItem = gl.DropdownUtils.filterHint(input, {
- hint: 'label',
- });
- expect(updatedItem.droplab_hidden).toBe(false);
+ beforeEach(() => {
+ setFixtures(`
+ <ul class="tokens-container">
+ <li class="input-token">
+ <input class="filtered-search" type="text" id="test" />
+ </li>
+ </ul>
+ `);
- input.value = 'o';
- updatedItem = gl.DropdownUtils.filterHint(input, {
- hint: 'label',
- });
- expect(updatedItem.droplab_hidden).toBe(true);
- });
+ input = document.getElementById('test');
+ });
- it('should return droplab_hidden false when item has no hint', () => {
- const updatedItem = gl.DropdownUtils.filterHint(input, {}, '');
- expect(updatedItem.droplab_hidden).toBe(false);
+ it('should filter', () => {
+ input.value = 'l';
+ let updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'label',
});
+ expect(updatedItem.droplab_hidden).toBe(false);
- it('should allow multiple if item.type is array', () => {
- input.value = 'label:~first la';
- const updatedItem = gl.DropdownUtils.filterHint(input, {
- hint: 'label',
- type: 'array',
- });
- expect(updatedItem.droplab_hidden).toBe(false);
+ input.value = 'o';
+ updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'label',
});
+ expect(updatedItem.droplab_hidden).toBe(true);
+ });
- it('should prevent multiple if item.type is not array', () => {
- input.value = 'milestone:~first mile';
- let updatedItem = gl.DropdownUtils.filterHint(input, {
- hint: 'milestone',
- });
- expect(updatedItem.droplab_hidden).toBe(true);
+ it('should return droplab_hidden false when item has no hint', () => {
+ const updatedItem = gl.DropdownUtils.filterHint(input, {}, '');
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
- updatedItem = gl.DropdownUtils.filterHint(input, {
- hint: 'milestone',
- type: 'string',
- });
- expect(updatedItem.droplab_hidden).toBe(true);
+ it('should allow multiple if item.type is array', () => {
+ input.value = 'label:~first la';
+ const updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'label',
+ type: 'array',
});
+ expect(updatedItem.droplab_hidden).toBe(false);
});
- describe('setDataValueIfSelected', () => {
- beforeEach(() => {
- spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput')
- .and.callFake(() => {});
+ it('should prevent multiple if item.type is not array', () => {
+ input.value = 'milestone:~first mile';
+ let updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'milestone',
});
+ expect(updatedItem.droplab_hidden).toBe(true);
- it('calls addWordToInput when dataValue exists', () => {
- const selected = {
- getAttribute: () => 'value',
- };
-
- gl.DropdownUtils.setDataValueIfSelected(null, selected);
- expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
+ updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'milestone',
+ type: 'string',
});
+ expect(updatedItem.droplab_hidden).toBe(true);
+ });
+ });
- it('returns true when dataValue exists', () => {
- const selected = {
- getAttribute: () => 'value',
- };
+ describe('setDataValueIfSelected', () => {
+ beforeEach(() => {
+ spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput')
+ .and.callFake(() => {});
+ });
- const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
- expect(result).toBe(true);
- });
+ it('calls addWordToInput when dataValue exists', () => {
+ const selected = {
+ getAttribute: () => 'value',
+ };
- it('returns false when dataValue does not exist', () => {
- const selected = {
- getAttribute: () => null,
- };
+ gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
+ });
- const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
- expect(result).toBe(false);
- });
+ it('returns true when dataValue exists', () => {
+ const selected = {
+ getAttribute: () => 'value',
+ };
+
+ const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(result).toBe(true);
});
- describe('getInputSelectionPosition', () => {
- describe('word with trailing spaces', () => {
- const value = 'label:none ';
+ it('returns false when dataValue does not exist', () => {
+ const selected = {
+ getAttribute: () => null,
+ };
+
+ const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(result).toBe(false);
+ });
+ });
- it('should return selectionStart when cursor is at the trailing space', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 11,
- value,
- });
+ describe('getInputSelectionPosition', () => {
+ describe('word with trailing spaces', () => {
+ const value = 'label:none ';
- expect(left).toBe(11);
- expect(right).toBe(11);
+ it('should return selectionStart when cursor is at the trailing space', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 11,
+ value,
});
- it('should return input when cursor is at the start of input', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 0,
- value,
- });
+ expect(left).toBe(11);
+ expect(right).toBe(11);
+ });
- expect(left).toBe(0);
- expect(right).toBe(10);
+ it('should return input when cursor is at the start of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 0,
+ value,
});
- it('should return input when cursor is at the middle of input', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 7,
- value,
- });
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
- expect(left).toBe(0);
- expect(right).toBe(10);
+ it('should return input when cursor is at the middle of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 7,
+ value,
});
- it('should return input when cursor is at the end of input', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 10,
- value,
- });
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
- expect(left).toBe(0);
- expect(right).toBe(10);
+ it('should return input when cursor is at the end of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 10,
+ value,
});
- });
- describe('multiple words', () => {
- const value = 'label:~"Community Contribution"';
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
+ });
- it('should return input when cursor is after the first word', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 17,
- value,
- });
+ describe('multiple words', () => {
+ const value = 'label:~"Community Contribution"';
- expect(left).toBe(0);
- expect(right).toBe(31);
+ it('should return input when cursor is after the first word', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 17,
+ value,
});
- it('should return input when cursor is before the second word', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 18,
- value,
- });
+ expect(left).toBe(0);
+ expect(right).toBe(31);
+ });
- expect(left).toBe(0);
- expect(right).toBe(31);
+ it('should return input when cursor is before the second word', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 18,
+ value,
});
- });
- describe('incomplete multiple words', () => {
- const value = 'label:~"Community Contribution';
+ expect(left).toBe(0);
+ expect(right).toBe(31);
+ });
+ });
- it('should return entire input when cursor is at the start of input', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 0,
- value,
- });
+ describe('incomplete multiple words', () => {
+ const value = 'label:~"Community Contribution';
- expect(left).toBe(0);
- expect(right).toBe(30);
+ it('should return entire input when cursor is at the start of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 0,
+ value,
});
- it('should return entire input when cursor is at the end of input', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 30,
- value,
- });
+ expect(left).toBe(0);
+ expect(right).toBe(30);
+ });
- expect(left).toBe(0);
- expect(right).toBe(30);
+ it('should return entire input when cursor is at the end of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 30,
+ value,
});
+
+ expect(left).toBe(0);
+ expect(right).toBe(30);
});
});
});
-})();
+});
diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
index a1da3396d7b..17bf8932489 100644
--- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
@@ -3,99 +3,97 @@ require('~/filtered_search/filtered_search_visual_tokens');
require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager');
-(() => {
- describe('Filtered Search Dropdown Manager', () => {
- describe('addWordToInput', () => {
- function getInputValue() {
- return document.querySelector('.filtered-search').value;
- }
-
- function setInputValue(value) {
- document.querySelector('.filtered-search').value = value;
- }
-
- beforeEach(() => {
- setFixtures(`
- <ul class="tokens-container">
- <li class="input-token">
- <input class="filtered-search">
- </li>
- </ul>
- `);
- });
+describe('Filtered Search Dropdown Manager', () => {
+ describe('addWordToInput', () => {
+ function getInputValue() {
+ return document.querySelector('.filtered-search').value;
+ }
+
+ function setInputValue(value) {
+ document.querySelector('.filtered-search').value = value;
+ }
+
+ beforeEach(() => {
+ setFixtures(`
+ <ul class="tokens-container">
+ <li class="input-token">
+ <input class="filtered-search">
+ </li>
+ </ul>
+ `);
+ });
- describe('input has no existing value', () => {
- it('should add just tokenName', () => {
- gl.FilteredSearchDropdownManager.addWordToInput('milestone');
+ describe('input has no existing value', () => {
+ it('should add just tokenName', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('milestone');
- const token = document.querySelector('.tokens-container .js-visual-token');
+ const token = document.querySelector('.tokens-container .js-visual-token');
- expect(token.classList.contains('filtered-search-token')).toEqual(true);
- expect(token.querySelector('.name').innerText).toBe('milestone');
- expect(getInputValue()).toBe('');
- });
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('milestone');
+ expect(getInputValue()).toBe('');
+ });
- it('should add tokenName and tokenValue', () => {
- gl.FilteredSearchDropdownManager.addWordToInput('label');
+ it('should add tokenName and tokenValue', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('label');
- let token = document.querySelector('.tokens-container .js-visual-token');
+ let token = document.querySelector('.tokens-container .js-visual-token');
- expect(token.classList.contains('filtered-search-token')).toEqual(true);
- expect(token.querySelector('.name').innerText).toBe('label');
- expect(getInputValue()).toBe('');
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('label');
+ expect(getInputValue()).toBe('');
- gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
- // We have to get that reference again
- // Because gl.FilteredSearchDropdownManager deletes the previous token
- token = document.querySelector('.tokens-container .js-visual-token');
+ gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
+ // We have to get that reference again
+ // Because gl.FilteredSearchDropdownManager deletes the previous token
+ token = document.querySelector('.tokens-container .js-visual-token');
- expect(token.classList.contains('filtered-search-token')).toEqual(true);
- expect(token.querySelector('.name').innerText).toBe('label');
- expect(token.querySelector('.value').innerText).toBe('none');
- expect(getInputValue()).toBe('');
- });
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('label');
+ expect(token.querySelector('.value').innerText).toBe('none');
+ expect(getInputValue()).toBe('');
});
+ });
- describe('input has existing value', () => {
- it('should be able to just add tokenName', () => {
- setInputValue('a');
- gl.FilteredSearchDropdownManager.addWordToInput('author');
+ describe('input has existing value', () => {
+ it('should be able to just add tokenName', () => {
+ setInputValue('a');
+ gl.FilteredSearchDropdownManager.addWordToInput('author');
- const token = document.querySelector('.tokens-container .js-visual-token');
+ const token = document.querySelector('.tokens-container .js-visual-token');
- expect(token.classList.contains('filtered-search-token')).toEqual(true);
- expect(token.querySelector('.name').innerText).toBe('author');
- expect(getInputValue()).toBe('');
- });
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('author');
+ expect(getInputValue()).toBe('');
+ });
- it('should replace tokenValue', () => {
- gl.FilteredSearchDropdownManager.addWordToInput('author');
+ it('should replace tokenValue', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('author');
- setInputValue('roo');
- gl.FilteredSearchDropdownManager.addWordToInput(null, '@root');
+ setInputValue('roo');
+ gl.FilteredSearchDropdownManager.addWordToInput(null, '@root');
- const token = document.querySelector('.tokens-container .js-visual-token');
+ const token = document.querySelector('.tokens-container .js-visual-token');
- expect(token.classList.contains('filtered-search-token')).toEqual(true);
- expect(token.querySelector('.name').innerText).toBe('author');
- expect(token.querySelector('.value').innerText).toBe('@root');
- expect(getInputValue()).toBe('');
- });
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('author');
+ expect(token.querySelector('.value').innerText).toBe('@root');
+ expect(getInputValue()).toBe('');
+ });
- it('should add tokenValues containing spaces', () => {
- gl.FilteredSearchDropdownManager.addWordToInput('label');
+ it('should add tokenValues containing spaces', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('label');
- setInputValue('"test ');
- gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
+ setInputValue('"test ');
+ gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
- const token = document.querySelector('.tokens-container .js-visual-token');
+ const token = document.querySelector('.tokens-container .js-visual-token');
- expect(token.classList.contains('filtered-search-token')).toEqual(true);
- expect(token.querySelector('.name').innerText).toBe('label');
- expect(token.querySelector('.value').innerText).toBe('~\'"test me"\'');
- expect(getInputValue()).toBe('');
- });
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('label');
+ expect(token.querySelector('.value').innerText).toBe('~\'"test me"\'');
+ expect(getInputValue()).toBe('');
});
});
});
-})();
+});
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 97af681429b..6683489f63c 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -6,271 +6,269 @@ require('~/filtered_search/filtered_search_dropdown_manager');
require('~/filtered_search/filtered_search_manager');
const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
-(() => {
- describe('Filtered Search Manager', () => {
- let input;
- let manager;
- let tokensContainer;
- const placeholder = 'Search or filter results...';
-
- function dispatchBackspaceEvent(element, eventType) {
- const backspaceKey = 8;
- const event = new Event(eventType);
- event.keyCode = backspaceKey;
- element.dispatchEvent(event);
- }
+describe('Filtered Search Manager', () => {
+ let input;
+ let manager;
+ let tokensContainer;
+ const placeholder = 'Search or filter results...';
+
+ function dispatchBackspaceEvent(element, eventType) {
+ const backspaceKey = 8;
+ const event = new Event(eventType);
+ event.keyCode = backspaceKey;
+ element.dispatchEvent(event);
+ }
+
+ function dispatchDeleteEvent(element, eventType) {
+ const deleteKey = 46;
+ const event = new Event(eventType);
+ event.keyCode = deleteKey;
+ element.dispatchEvent(event);
+ }
+
+ beforeEach(() => {
+ setFixtures(`
+ <div class="filtered-search-box">
+ <form>
+ <ul class="tokens-container list-unstyled">
+ ${FilteredSearchSpecHelper.createInputHTML(placeholder)}
+ </ul>
+ <button class="clear-search" type="button">
+ <i class="fa fa-times"></i>
+ </button>
+ </form>
+ </div>
+ `);
+
+ spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
+ spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
+ spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
+ spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
+ spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
+ spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
+
+ input = document.querySelector('.filtered-search');
+ tokensContainer = document.querySelector('.tokens-container');
+ manager = new gl.FilteredSearchManager();
+ });
- function dispatchDeleteEvent(element, eventType) {
- const deleteKey = 46;
- const event = new Event(eventType);
- event.keyCode = deleteKey;
- element.dispatchEvent(event);
- }
+ afterEach(() => {
+ manager.cleanup();
+ });
- beforeEach(() => {
- setFixtures(`
- <div class="filtered-search-box">
- <form>
- <ul class="tokens-container list-unstyled">
- ${FilteredSearchSpecHelper.createInputHTML(placeholder)}
- </ul>
- <button class="clear-search" type="button">
- <i class="fa fa-times"></i>
- </button>
- </form>
- </div>
- `);
+ describe('search', () => {
+ const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
- spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
- spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
- spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
- spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
- spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
- spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
+ it('should search with a single word', (done) => {
+ input.value = 'searchTerm';
- input = document.querySelector('.filtered-search');
- tokensContainer = document.querySelector('.tokens-container');
- manager = new gl.FilteredSearchManager();
- });
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&search=searchTerm`);
+ done();
+ });
- afterEach(() => {
- manager.cleanup();
+ manager.search();
});
- describe('search', () => {
- const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
-
- it('should search with a single word', (done) => {
- input.value = 'searchTerm';
+ it('should search with multiple words', (done) => {
+ input.value = 'awesome search terms';
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
- expect(url).toEqual(`${defaultParams}&search=searchTerm`);
- done();
- });
-
- manager.search();
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
+ done();
});
- it('should search with multiple words', (done) => {
- input.value = 'awesome search terms';
+ manager.search();
+ });
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
- expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
- done();
- });
+ it('should search with special characters', (done) => {
+ input.value = '~!@#$%^&*()_+{}:<>,.?/';
- manager.search();
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
+ done();
});
- it('should search with special characters', (done) => {
- input.value = '~!@#$%^&*()_+{}:<>,.?/';
+ manager.search();
+ });
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
- expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
- done();
- });
+ it('removes duplicated tokens', (done) => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ `);
- manager.search();
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
+ done();
});
- it('removes duplicated tokens', (done) => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
- `);
-
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
- expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
- done();
- });
+ manager.search();
+ });
+ });
- manager.search();
- });
+ describe('handleInputPlaceholder', () => {
+ it('should render placeholder when there is no input', () => {
+ expect(input.placeholder).toEqual(placeholder);
});
- describe('handleInputPlaceholder', () => {
- it('should render placeholder when there is no input', () => {
- expect(input.placeholder).toEqual(placeholder);
- });
+ it('should not render placeholder when there is input', () => {
+ input.value = 'test words';
+
+ const event = new Event('input');
+ input.dispatchEvent(event);
- it('should not render placeholder when there is input', () => {
- input.value = 'test words';
+ expect(input.placeholder).toEqual('');
+ });
- const event = new Event('input');
- input.dispatchEvent(event);
+ it('should not render placeholder when there are tokens and no input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ );
- expect(input.placeholder).toEqual('');
- });
+ const event = new Event('input');
+ input.dispatchEvent(event);
- it('should not render placeholder when there are tokens and no input', () => {
+ expect(input.placeholder).toEqual('');
+ });
+ });
+
+ describe('checkForBackspace', () => {
+ describe('tokens and no input', () => {
+ beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
);
-
- const event = new Event('input');
- input.dispatchEvent(event);
-
- expect(input.placeholder).toEqual('');
});
- });
-
- describe('checkForBackspace', () => {
- describe('tokens and no input', () => {
- beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
- );
- });
- it('removes last token', () => {
- spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
- dispatchBackspaceEvent(input, 'keyup');
-
- expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
- });
-
- it('sets the input', () => {
- spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
- dispatchDeleteEvent(input, 'keyup');
+ it('removes last token', () => {
+ spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
+ dispatchBackspaceEvent(input, 'keyup');
- expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
- expect(input.value).toEqual('~bug');
- });
+ expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
});
- it('does not remove token or change input when there is existing input', () => {
- spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
+ it('sets the input', () => {
spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
-
- input.value = 'text';
dispatchDeleteEvent(input, 'keyup');
- expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
- expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
- expect(input.value).toEqual('text');
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
+ expect(input.value).toEqual('~bug');
});
});
- describe('removeSelectedToken', () => {
- function getVisualTokens() {
- return tokensContainer.querySelectorAll('.js-visual-token');
- }
+ it('does not remove token or change input when there is existing input', () => {
+ spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
+ spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
- beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
- );
- });
+ input.value = 'text';
+ dispatchDeleteEvent(input, 'keyup');
- it('removes selected token when the backspace key is pressed', () => {
- expect(getVisualTokens().length).toEqual(1);
+ expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('text');
+ });
+ });
- dispatchBackspaceEvent(document, 'keydown');
+ describe('removeSelectedToken', () => {
+ function getVisualTokens() {
+ return tokensContainer.querySelectorAll('.js-visual-token');
+ }
- expect(getVisualTokens().length).toEqual(0);
- });
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+ );
+ });
- it('removes selected token when the delete key is pressed', () => {
- expect(getVisualTokens().length).toEqual(1);
+ it('removes selected token when the backspace key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
- dispatchDeleteEvent(document, 'keydown');
+ dispatchBackspaceEvent(document, 'keydown');
- expect(getVisualTokens().length).toEqual(0);
- });
+ expect(getVisualTokens().length).toEqual(0);
+ });
- it('updates the input placeholder after removal', () => {
- manager.handleInputPlaceholder();
+ it('removes selected token when the delete key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
- expect(input.placeholder).toEqual('');
- expect(getVisualTokens().length).toEqual(1);
+ dispatchDeleteEvent(document, 'keydown');
- dispatchBackspaceEvent(document, 'keydown');
+ expect(getVisualTokens().length).toEqual(0);
+ });
- expect(input.placeholder).not.toEqual('');
- expect(getVisualTokens().length).toEqual(0);
- });
+ it('updates the input placeholder after removal', () => {
+ manager.handleInputPlaceholder();
- it('updates the clear button after removal', () => {
- manager.toggleClearSearchButton();
+ expect(input.placeholder).toEqual('');
+ expect(getVisualTokens().length).toEqual(1);
- const clearButton = document.querySelector('.clear-search');
+ dispatchBackspaceEvent(document, 'keydown');
- expect(clearButton.classList.contains('hidden')).toEqual(false);
- expect(getVisualTokens().length).toEqual(1);
+ expect(input.placeholder).not.toEqual('');
+ expect(getVisualTokens().length).toEqual(0);
+ });
- dispatchBackspaceEvent(document, 'keydown');
+ it('updates the clear button after removal', () => {
+ manager.toggleClearSearchButton();
- expect(clearButton.classList.contains('hidden')).toEqual(true);
- expect(getVisualTokens().length).toEqual(0);
- });
+ const clearButton = document.querySelector('.clear-search');
+
+ expect(clearButton.classList.contains('hidden')).toEqual(false);
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(clearButton.classList.contains('hidden')).toEqual(true);
+ expect(getVisualTokens().length).toEqual(0);
});
+ });
- describe('unselects token', () => {
- beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)}
- ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
- `);
- });
+ describe('unselects token', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
+ `);
+ });
- it('unselects token when input is clicked', () => {
- const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+ it('unselects token when input is clicked', () => {
+ const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
- expect(selectedToken.classList.contains('selected')).toEqual(true);
- expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
+ expect(selectedToken.classList.contains('selected')).toEqual(true);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
- // Click directly on input attached to document
- // so that the click event will propagate properly
- document.querySelector('.filtered-search').click();
+ // Click directly on input attached to document
+ // so that the click event will propagate properly
+ document.querySelector('.filtered-search').click();
- expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
- expect(selectedToken.classList.contains('selected')).toEqual(false);
- });
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
+ expect(selectedToken.classList.contains('selected')).toEqual(false);
+ });
- it('unselects token when document.body is clicked', () => {
- const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+ it('unselects token when document.body is clicked', () => {
+ const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
- expect(selectedToken.classList.contains('selected')).toEqual(true);
- expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
+ expect(selectedToken.classList.contains('selected')).toEqual(true);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
- document.body.click();
+ document.body.click();
- expect(selectedToken.classList.contains('selected')).toEqual(false);
- expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
- });
+ expect(selectedToken.classList.contains('selected')).toEqual(false);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
});
+ });
- describe('toggleInputContainerFocus', () => {
- it('toggles on focus', () => {
- input.focus();
- expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true);
- });
+ describe('toggleInputContainerFocus', () => {
+ it('toggles on focus', () => {
+ input.focus();
+ expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true);
+ });
- it('toggles on blur', () => {
- input.blur();
- expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false);
- });
+ it('toggles on blur', () => {
+ input.blur();
+ expect(document.querySelector('.filtered-search-box').classList.contains('focus')).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 cf409a7e509..6f9fa434c35 100644
--- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
@@ -1,110 +1,108 @@
require('~/extensions/array');
require('~/filtered_search/filtered_search_token_keys');
-(() => {
- describe('Filtered Search Token Keys', () => {
- describe('get', () => {
- let tokenKeys;
-
- beforeEach(() => {
- tokenKeys = gl.FilteredSearchTokenKeys.get();
- });
-
- it('should return tokenKeys', () => {
- expect(tokenKeys !== null).toBe(true);
- });
-
- it('should return tokenKeys as an array', () => {
- expect(tokenKeys instanceof Array).toBe(true);
- });
- });
-
- describe('getConditions', () => {
- let conditions;
-
- beforeEach(() => {
- conditions = gl.FilteredSearchTokenKeys.getConditions();
- });
-
- it('should return conditions', () => {
- expect(conditions !== null).toBe(true);
- });
-
- it('should return conditions as an array', () => {
- expect(conditions instanceof Array).toBe(true);
- });
- });
-
- describe('searchByKey', () => {
- it('should return null when key not found', () => {
- const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey');
- expect(tokenKey === null).toBe(true);
- });
-
- it('should return tokenKey when found by key', () => {
- const tokenKeys = gl.FilteredSearchTokenKeys.get();
- const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key);
- expect(result).toEqual(tokenKeys[0]);
- });
- });
-
- describe('searchBySymbol', () => {
- it('should return null when symbol not found', () => {
- const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol');
- expect(tokenKey === null).toBe(true);
- });
-
- it('should return tokenKey when found by symbol', () => {
- const tokenKeys = gl.FilteredSearchTokenKeys.get();
- const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol);
- expect(result).toEqual(tokenKeys[0]);
- });
- });
-
- describe('searchByKeyParam', () => {
- it('should return null when key param not found', () => {
- const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam');
- expect(tokenKey === null).toBe(true);
- });
-
- it('should return tokenKey when found by key param', () => {
- const tokenKeys = gl.FilteredSearchTokenKeys.get();
- const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
- expect(result).toEqual(tokenKeys[0]);
- });
-
- it('should return alternative tokenKey when found by key param', () => {
- const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives();
- const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
- expect(result).toEqual(tokenKeys[0]);
- });
- });
-
- describe('searchByConditionUrl', () => {
- it('should return null when condition url not found', () => {
- const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null);
- expect(condition === null).toBe(true);
- });
-
- it('should return condition when found by url', () => {
- const conditions = gl.FilteredSearchTokenKeys.getConditions();
- const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url);
- expect(result).toBe(conditions[0]);
- });
- });
-
- describe('searchByConditionKeyValue', () => {
- it('should return null when condition tokenKey and value not found', () => {
- const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null);
- expect(condition === null).toBe(true);
- });
-
- it('should return condition when found by tokenKey and value', () => {
- const conditions = gl.FilteredSearchTokenKeys.getConditions();
- const result = gl.FilteredSearchTokenKeys
- .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value);
- expect(result).toEqual(conditions[0]);
- });
+describe('Filtered Search Token Keys', () => {
+ describe('get', () => {
+ let tokenKeys;
+
+ beforeEach(() => {
+ tokenKeys = gl.FilteredSearchTokenKeys.get();
+ });
+
+ it('should return tokenKeys', () => {
+ expect(tokenKeys !== null).toBe(true);
+ });
+
+ it('should return tokenKeys as an array', () => {
+ expect(tokenKeys instanceof Array).toBe(true);
+ });
+ });
+
+ describe('getConditions', () => {
+ let conditions;
+
+ beforeEach(() => {
+ conditions = gl.FilteredSearchTokenKeys.getConditions();
+ });
+
+ it('should return conditions', () => {
+ expect(conditions !== null).toBe(true);
+ });
+
+ it('should return conditions as an array', () => {
+ expect(conditions instanceof Array).toBe(true);
+ });
+ });
+
+ describe('searchByKey', () => {
+ it('should return null when key not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by key', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchBySymbol', () => {
+ it('should return null when symbol not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by symbol', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchByKeyParam', () => {
+ it('should return null when key param not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by key param', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+
+ it('should return alternative tokenKey when found by key param', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives();
+ const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchByConditionUrl', () => {
+ it('should return null when condition url not found', () => {
+ const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null);
+ expect(condition === null).toBe(true);
+ });
+
+ it('should return condition when found by url', () => {
+ const conditions = gl.FilteredSearchTokenKeys.getConditions();
+ const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url);
+ expect(result).toBe(conditions[0]);
+ });
+ });
+
+ describe('searchByConditionKeyValue', () => {
+ it('should return null when condition tokenKey and value not found', () => {
+ const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null);
+ expect(condition === null).toBe(true);
+ });
+
+ it('should return condition when found by tokenKey and value', () => {
+ const conditions = gl.FilteredSearchTokenKeys.getConditions();
+ const result = gl.FilteredSearchTokenKeys
+ .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value);
+ expect(result).toEqual(conditions[0]);
});
});
-})();
+});
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
index cabbc694ec4..3e2e577f115 100644
--- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
@@ -2,134 +2,132 @@ require('~/extensions/array');
require('~/filtered_search/filtered_search_token_keys');
require('~/filtered_search/filtered_search_tokenizer');
-(() => {
- describe('Filtered Search Tokenizer', () => {
- describe('processTokens', () => {
- it('returns for input containing only search value', () => {
- const results = gl.FilteredSearchTokenizer.processTokens('searchTerm');
- expect(results.searchToken).toBe('searchTerm');
- expect(results.tokens.length).toBe(0);
- expect(results.lastToken).toBe(results.searchToken);
- });
-
- it('returns for input containing only tokens', () => {
- const results = gl.FilteredSearchTokenizer
- .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none');
- expect(results.searchToken).toBe('');
- expect(results.tokens.length).toBe(4);
- expect(results.tokens[3]).toBe(results.lastToken);
-
- expect(results.tokens[0].key).toBe('author');
- expect(results.tokens[0].value).toBe('root');
- expect(results.tokens[0].symbol).toBe('@');
-
- expect(results.tokens[1].key).toBe('label');
- expect(results.tokens[1].value).toBe('"Very Important"');
- expect(results.tokens[1].symbol).toBe('~');
-
- expect(results.tokens[2].key).toBe('milestone');
- expect(results.tokens[2].value).toBe('v1.0');
- expect(results.tokens[2].symbol).toBe('%');
-
- expect(results.tokens[3].key).toBe('assignee');
- expect(results.tokens[3].value).toBe('none');
- expect(results.tokens[3].symbol).toBe('');
- });
-
- it('returns for input starting with search value and ending with tokens', () => {
- const results = gl.FilteredSearchTokenizer
- .processTokens('searchTerm anotherSearchTerm milestone:none');
- expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
- expect(results.tokens.length).toBe(1);
- expect(results.tokens[0]).toBe(results.lastToken);
- expect(results.tokens[0].key).toBe('milestone');
- expect(results.tokens[0].value).toBe('none');
- expect(results.tokens[0].symbol).toBe('');
- });
-
- it('returns for input starting with tokens and ending with search value', () => {
- const results = gl.FilteredSearchTokenizer
- .processTokens('assignee:@user searchTerm');
-
- expect(results.searchToken).toBe('searchTerm');
- expect(results.tokens.length).toBe(1);
- expect(results.tokens[0].key).toBe('assignee');
- expect(results.tokens[0].value).toBe('user');
- expect(results.tokens[0].symbol).toBe('@');
- expect(results.lastToken).toBe(results.searchToken);
- });
-
- it('returns for input containing search value wrapped between tokens', () => {
- const results = gl.FilteredSearchTokenizer
- .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none');
-
- expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
- expect(results.tokens.length).toBe(3);
- expect(results.tokens[2]).toBe(results.lastToken);
-
- expect(results.tokens[0].key).toBe('author');
- expect(results.tokens[0].value).toBe('root');
- expect(results.tokens[0].symbol).toBe('@');
-
- expect(results.tokens[1].key).toBe('label');
- expect(results.tokens[1].value).toBe('"Won\'t fix"');
- expect(results.tokens[1].symbol).toBe('~');
-
- expect(results.tokens[2].key).toBe('milestone');
- expect(results.tokens[2].value).toBe('none');
- expect(results.tokens[2].symbol).toBe('');
- });
-
- it('returns for input containing search value in between tokens', () => {
- const results = gl.FilteredSearchTokenizer
- .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing');
- expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
- expect(results.tokens.length).toBe(3);
- expect(results.tokens[2]).toBe(results.lastToken);
-
- expect(results.tokens[0].key).toBe('author');
- expect(results.tokens[0].value).toBe('root');
- expect(results.tokens[0].symbol).toBe('@');
-
- expect(results.tokens[1].key).toBe('assignee');
- expect(results.tokens[1].value).toBe('none');
- expect(results.tokens[1].symbol).toBe('');
-
- expect(results.tokens[2].key).toBe('label');
- expect(results.tokens[2].value).toBe('Doing');
- expect(results.tokens[2].symbol).toBe('~');
- });
-
- it('returns search value for invalid tokens', () => {
- const results = gl.FilteredSearchTokenizer.processTokens('fake:token');
- expect(results.lastToken).toBe('fake:token');
- expect(results.searchToken).toBe('fake:token');
- expect(results.tokens.length).toEqual(0);
- });
-
- it('returns search value and token for mix of valid and invalid tokens', () => {
- const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token');
- expect(results.tokens.length).toEqual(1);
- expect(results.tokens[0].key).toBe('label');
- expect(results.tokens[0].value).toBe('real');
- expect(results.tokens[0].symbol).toBe('');
- expect(results.lastToken).toBe('fake:token');
- expect(results.searchToken).toBe('fake:token');
- });
-
- it('returns search value for invalid symbols', () => {
- const results = gl.FilteredSearchTokenizer.processTokens('std::includes');
- expect(results.lastToken).toBe('std::includes');
- expect(results.searchToken).toBe('std::includes');
- });
-
- it('removes duplicated values', () => {
- const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo');
- expect(results.tokens.length).toBe(1);
- expect(results.tokens[0].key).toBe('label');
- expect(results.tokens[0].value).toBe('foo');
- expect(results.tokens[0].symbol).toBe('~');
- });
+describe('Filtered Search Tokenizer', () => {
+ describe('processTokens', () => {
+ it('returns for input containing only search value', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('searchTerm');
+ expect(results.searchToken).toBe('searchTerm');
+ expect(results.tokens.length).toBe(0);
+ expect(results.lastToken).toBe(results.searchToken);
+ });
+
+ it('returns for input containing only tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none');
+ expect(results.searchToken).toBe('');
+ expect(results.tokens.length).toBe(4);
+ expect(results.tokens[3]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('label');
+ expect(results.tokens[1].value).toBe('"Very Important"');
+ expect(results.tokens[1].symbol).toBe('~');
+
+ expect(results.tokens[2].key).toBe('milestone');
+ expect(results.tokens[2].value).toBe('v1.0');
+ expect(results.tokens[2].symbol).toBe('%');
+
+ expect(results.tokens[3].key).toBe('assignee');
+ expect(results.tokens[3].value).toBe('none');
+ expect(results.tokens[3].symbol).toBe('');
+ });
+
+ it('returns for input starting with search value and ending with tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('searchTerm anotherSearchTerm milestone:none');
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(1);
+ expect(results.tokens[0]).toBe(results.lastToken);
+ expect(results.tokens[0].key).toBe('milestone');
+ expect(results.tokens[0].value).toBe('none');
+ expect(results.tokens[0].symbol).toBe('');
+ });
+
+ it('returns for input starting with tokens and ending with search value', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('assignee:@user searchTerm');
+
+ expect(results.searchToken).toBe('searchTerm');
+ expect(results.tokens.length).toBe(1);
+ expect(results.tokens[0].key).toBe('assignee');
+ expect(results.tokens[0].value).toBe('user');
+ expect(results.tokens[0].symbol).toBe('@');
+ expect(results.lastToken).toBe(results.searchToken);
+ });
+
+ it('returns for input containing search value wrapped between tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none');
+
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(3);
+ expect(results.tokens[2]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('label');
+ expect(results.tokens[1].value).toBe('"Won\'t fix"');
+ expect(results.tokens[1].symbol).toBe('~');
+
+ expect(results.tokens[2].key).toBe('milestone');
+ expect(results.tokens[2].value).toBe('none');
+ expect(results.tokens[2].symbol).toBe('');
+ });
+
+ it('returns for input containing search value in between tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing');
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(3);
+ expect(results.tokens[2]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('assignee');
+ expect(results.tokens[1].value).toBe('none');
+ expect(results.tokens[1].symbol).toBe('');
+
+ expect(results.tokens[2].key).toBe('label');
+ expect(results.tokens[2].value).toBe('Doing');
+ expect(results.tokens[2].symbol).toBe('~');
+ });
+
+ it('returns search value for invalid tokens', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('fake:token');
+ expect(results.lastToken).toBe('fake:token');
+ expect(results.searchToken).toBe('fake:token');
+ expect(results.tokens.length).toEqual(0);
+ });
+
+ it('returns search value and token for mix of valid and invalid tokens', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token');
+ expect(results.tokens.length).toEqual(1);
+ expect(results.tokens[0].key).toBe('label');
+ expect(results.tokens[0].value).toBe('real');
+ expect(results.tokens[0].symbol).toBe('');
+ expect(results.lastToken).toBe('fake:token');
+ expect(results.searchToken).toBe('fake:token');
+ });
+
+ it('returns search value for invalid symbols', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('std::includes');
+ expect(results.lastToken).toBe('std::includes');
+ expect(results.searchToken).toBe('std::includes');
+ });
+
+ it('removes duplicated values', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo');
+ expect(results.tokens.length).toBe(1);
+ expect(results.tokens[0].key).toBe('label');
+ expect(results.tokens[0].value).toBe('foo');
+ expect(results.tokens[0].symbol).toBe('~');
});
});
-})();
+});
diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js
index 2a58fb3a7df..c255bf7c939 100644
--- a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js
+++ b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js
@@ -1,3 +1,5 @@
+/* eslint-disable promise/catch-or-return */
+
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
describe('RecentSearchesService', () => {
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index fddeaaf504d..47d904b865b 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -7,6 +7,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') }
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') }
+ let(:merged_merge_request) { create(:merge_request, :merged, source_project: project, target_project: project) }
let(:pipeline) do
create(
:ci_pipeline,
@@ -32,6 +33,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
render_merge_request(example.description, merge_request)
end
+ it 'merge_requests/merged_merge_request.html.raw' do |example|
+ allow_any_instance_of(MergeRequest).to receive(:source_branch_exists?).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:can_remove_source_branch?).and_return(true)
+ render_merge_request(example.description, merged_merge_request)
+ end
+
private
def render_merge_request(fixture_file_name, merge_request)
diff --git a/spec/javascripts/issue_show/issue_title_spec.js b/spec/javascripts/issue_show/issue_title_spec.js
index 806d728a874..03edbf9f947 100644
--- a/spec/javascripts/issue_show/issue_title_spec.js
+++ b/spec/javascripts/issue_show/issue_title_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import issueTitle from '~/issue_show/issue_title';
+import issueTitle from '~/issue_show/issue_title.vue';
describe('Issue Title', () => {
let IssueTitleComponent;
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index aabc8bea12f..9a2570ef7e9 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,18 +1,17 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
+/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
import Issue from '~/issue';
require('~/lib/utils/text_utility');
describe('Issue', function() {
- var INVALID_URL = 'http://goesnowhere.nothing/whereami';
- var $boxClosed, $boxOpen, $btnClose, $btnReopen;
+ let $boxClosed, $boxOpen, $btnClose, $btnReopen;
preloadFixtures('issues/closed-issue.html.raw');
preloadFixtures('issues/issue-with-task-list.html.raw');
preloadFixtures('issues/open-issue.html.raw');
function expectErrorMessage() {
- var $flashMessage = $('div.flash-alert');
+ const $flashMessage = $('div.flash-alert');
expect($flashMessage).toExist();
expect($flashMessage).toBeVisible();
expect($flashMessage).toHaveText('Unable to update this issue at this time.');
@@ -26,10 +25,28 @@ describe('Issue', function() {
expectVisibility($btnReopen, !isIssueOpen);
}
- function expectPendingRequest(req, $triggeredButton) {
- expect(req.type).toBe('PUT');
- expect(req.url).toBe($triggeredButton.attr('href'));
- expect($triggeredButton).toHaveProp('disabled', true);
+ function expectNewBranchButtonState(isPending, canCreate) {
+ if (Issue.$btnNewBranch.length === 0) {
+ return;
+ }
+
+ const $available = Issue.$btnNewBranch.find('.available');
+ expect($available).toHaveText('New branch');
+
+ if (!isPending && canCreate) {
+ expect($available).toBeVisible();
+ } else {
+ expect($available).toBeHidden();
+ }
+
+ const $unavailable = Issue.$btnNewBranch.find('.unavailable');
+ expect($unavailable).toHaveText('New branch unavailable');
+
+ if (!isPending && !canCreate) {
+ expect($unavailable).toBeVisible();
+ } else {
+ expect($unavailable).toBeHidden();
+ }
}
function expectVisibility($element, shouldBeVisible) {
@@ -81,100 +98,107 @@ describe('Issue', function() {
});
});
- describe('close issue', function() {
- beforeEach(function() {
- loadFixtures('issues/open-issue.html.raw');
- findElements();
- this.issue = new Issue();
-
- expectIssueState(true);
- });
+ [true, false].forEach((isIssueInitiallyOpen) => {
+ describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() {
+ const action = isIssueInitiallyOpen ? 'close' : 'reopen';
+
+ function ajaxSpy(req) {
+ if (req.url === this.$triggeredButton.attr('href')) {
+ expect(req.type).toBe('PUT');
+ expect(this.$triggeredButton).toHaveProp('disabled', true);
+ expectNewBranchButtonState(true, false);
+ return this.issueStateDeferred;
+ } else if (req.url === Issue.$btnNewBranch.data('path')) {
+ expect(req.type).toBe('get');
+ expectNewBranchButtonState(true, false);
+ return this.canCreateBranchDeferred;
+ }
+
+ expect(req.url).toBe('unexpected');
+ return null;
+ }
+
+ beforeEach(function() {
+ if (isIssueInitiallyOpen) {
+ loadFixtures('issues/open-issue.html.raw');
+ } else {
+ loadFixtures('issues/closed-issue.html.raw');
+ }
+
+ findElements();
+ this.issue = new Issue();
+ expectIssueState(isIssueInitiallyOpen);
+ this.$triggeredButton = isIssueInitiallyOpen ? $btnClose : $btnReopen;
+
+ this.$projectIssuesCounter = $('.issue_counter');
+ this.$projectIssuesCounter.text('1,001');
+
+ this.issueStateDeferred = new jQuery.Deferred();
+ this.canCreateBranchDeferred = new jQuery.Deferred();
+
+ spyOn(jQuery, 'ajax').and.callFake(ajaxSpy.bind(this));
+ });
- it('closes an issue', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expectPendingRequest(req, $btnClose);
- req.success({
+ it(`${action}s the issue`, function() {
+ this.$triggeredButton.trigger('click');
+ this.issueStateDeferred.resolve({
id: 34
});
- });
-
- $btnClose.trigger('click');
+ this.canCreateBranchDeferred.resolve({
+ can_create_branch: !isIssueInitiallyOpen
+ });
- expectIssueState(false);
- expect($btnClose).toHaveProp('disabled', false);
- expect($('.issue_counter')).toHaveText(0);
- });
+ expectIssueState(!isIssueInitiallyOpen);
+ expect(this.$triggeredButton).toHaveProp('disabled', false);
+ expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
+ expectNewBranchButtonState(false, !isIssueInitiallyOpen);
+ });
- it('fails to close an issue with success:false', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expectPendingRequest(req, $btnClose);
- req.success({
+ it(`fails to ${action} the issue if saved:false`, function() {
+ this.$triggeredButton.trigger('click');
+ this.issueStateDeferred.resolve({
saved: false
});
- });
-
- $btnClose.attr('href', INVALID_URL);
- $btnClose.trigger('click');
-
- expectIssueState(true);
- expect($btnClose).toHaveProp('disabled', false);
- expectErrorMessage();
- expect($('.issue_counter')).toHaveText(1);
- });
+ this.canCreateBranchDeferred.resolve({
+ can_create_branch: isIssueInitiallyOpen
+ });
- it('fails to closes an issue with HTTP error', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expectPendingRequest(req, $btnClose);
- req.error();
+ expectIssueState(isIssueInitiallyOpen);
+ expect(this.$triggeredButton).toHaveProp('disabled', false);
+ expectErrorMessage();
+ expect(this.$projectIssuesCounter.text()).toBe('1,001');
+ expectNewBranchButtonState(false, isIssueInitiallyOpen);
});
- $btnClose.attr('href', INVALID_URL);
- $btnClose.trigger('click');
-
- expectIssueState(true);
- expect($btnClose).toHaveProp('disabled', true);
- expectErrorMessage();
- expect($('.issue_counter')).toHaveText(1);
- });
-
- it('updates counter', () => {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expectPendingRequest(req, $btnClose);
- req.success({
- id: 34
+ it(`fails to ${action} the issue if HTTP error occurs`, function() {
+ this.$triggeredButton.trigger('click');
+ this.issueStateDeferred.reject();
+ this.canCreateBranchDeferred.resolve({
+ can_create_branch: isIssueInitiallyOpen
});
- });
- expect($('.issue_counter')).toHaveText(1);
- $('.issue_counter').text('1,001');
- expect($('.issue_counter').text()).toEqual('1,001');
- $btnClose.trigger('click');
- expect($('.issue_counter').text()).toEqual('1,000');
- });
- });
+ expectIssueState(isIssueInitiallyOpen);
+ expect(this.$triggeredButton).toHaveProp('disabled', true);
+ expectErrorMessage();
+ expect(this.$projectIssuesCounter.text()).toBe('1,001');
+ expectNewBranchButtonState(false, isIssueInitiallyOpen);
+ });
- describe('reopen issue', function() {
- beforeEach(function() {
- loadFixtures('issues/closed-issue.html.raw');
- findElements();
- this.issue = new Issue();
+ it('disables the new branch button if Ajax call fails', function() {
+ this.$triggeredButton.trigger('click');
+ this.issueStateDeferred.reject();
+ this.canCreateBranchDeferred.reject();
- expectIssueState(false);
- });
-
- it('reopens an issue', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expectPendingRequest(req, $btnReopen);
- req.success({
- id: 34
- });
+ expectNewBranchButtonState(false, false);
});
- $btnReopen.trigger('click');
+ it('does not trigger Ajax call if new branch button is missing', function() {
+ Issue.$btnNewBranch = $();
+ this.canCreateBranchDeferred = null;
- expectIssueState(true);
- expect($btnReopen).toHaveProp('disabled', false);
- expect($('.issue_counter')).toHaveText(1);
+ this.$triggeredButton.trigger('click');
+ this.issueStateDeferred.reject();
+ });
});
});
});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 03f3c206f44..a00efa10119 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -1,3 +1,5 @@
+/* eslint-disable promise/catch-or-return */
+
require('~/lib/utils/common_utils');
(() => {
@@ -313,7 +315,7 @@ require('~/lib/utils/common_utils');
describe('gl.utils.setFavicon', () => {
it('should set page favicon to provided favicon', () => {
- const faviconName = 'custom_favicon';
+ const faviconPath = '//custom_favicon';
const fakeLink = {
setAttribute() {},
};
@@ -321,9 +323,9 @@ require('~/lib/utils/common_utils');
spyOn(window.document, 'getElementById').and.callFake(() => fakeLink);
spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => {
expect(attr).toEqual('href');
- expect(val.indexOf('/assets/custom_favicon.ico') > -1).toBe(true);
+ expect(val.indexOf(faviconPath) > -1).toBe(true);
});
- gl.utils.setFavicon(faviconName);
+ gl.utils.setFavicon(faviconPath);
});
});
@@ -345,13 +347,12 @@ require('~/lib/utils/common_utils');
describe('gl.utils.setCiStatusFavicon', () => {
it('should set page favicon to CI status favicon based on provided status', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1/status.json`;
- const FAVICON_PATH = 'ci_favicons/';
- const FAVICON = 'icon_status_success';
+ const FAVICON_PATH = '//icon_status_success';
const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub();
const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub();
spyOn($, 'ajax').and.callFake(function (options) {
- options.success({ icon: FAVICON });
- expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH + FAVICON);
+ options.success({ favicon: FAVICON_PATH });
+ expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH);
options.success();
expect(spyResetFavicon).toHaveBeenCalled();
options.error();
diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js
index 5fde8be9123..90b12c9f115 100644
--- a/spec/javascripts/lib/utils/number_utility_spec.js
+++ b/spec/javascripts/lib/utils/number_utility_spec.js
@@ -1,4 +1,4 @@
-import { formatRelevantDigits } from '~/lib/utils/number_utils';
+import { formatRelevantDigits, bytesToKiB } from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
@@ -38,4 +38,11 @@ describe('Number Utils', () => {
expect(leftFromDecimal.length).toBe(3);
});
});
+
+ describe('bytesToKiB', () => {
+ it('calculates KiB for the given bytes', () => {
+ expect(bytesToKiB(1024)).toEqual(1);
+ expect(bytesToKiB(1000)).toEqual(0.9765625);
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js
index e3429c2a1cb..918b6d32c43 100644
--- a/spec/javascripts/lib/utils/poll_spec.js
+++ b/spec/javascripts/lib/utils/poll_spec.js
@@ -4,6 +4,20 @@ import Poll from '~/lib/utils/poll';
Vue.use(VueResource);
+const waitForAllCallsToFinish = (service, waitForCount, successCallback) => {
+ const timer = () => {
+ setTimeout(() => {
+ if (service.fetch.calls.count() === waitForCount) {
+ successCallback();
+ } else {
+ timer();
+ }
+ }, 5);
+ };
+
+ timer();
+};
+
class ServiceMock {
constructor(endpoint) {
this.service = Vue.resource(endpoint);
@@ -16,6 +30,7 @@ class ServiceMock {
describe('Poll', () => {
let callbacks;
+ let service;
beforeEach(() => {
callbacks = {
@@ -23,8 +38,11 @@ describe('Poll', () => {
error: () => {},
};
+ service = new ServiceMock('endpoint');
+
spyOn(callbacks, 'success');
spyOn(callbacks, 'error');
+ spyOn(service, 'fetch').and.callThrough();
});
it('calls the success callback when no header for interval is provided', (done) => {
@@ -35,19 +53,20 @@ describe('Poll', () => {
Vue.http.interceptors.push(successInterceptor);
new Poll({
- resource: new ServiceMock('endpoint'),
+ resource: service,
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
- setTimeout(() => {
+ waitForAllCallsToFinish(service, 1, () => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
+
+ Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor);
+
done();
}, 0);
-
- Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor);
});
it('calls the error callback whe the http request returns an error', (done) => {
@@ -58,19 +77,19 @@ describe('Poll', () => {
Vue.http.interceptors.push(errorInterceptor);
new Poll({
- resource: new ServiceMock('endpoint'),
+ resource: service,
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
- setTimeout(() => {
+ waitForAllCallsToFinish(service, 1, () => {
expect(callbacks.success).not.toHaveBeenCalled();
expect(callbacks.error).toHaveBeenCalled();
- done();
- }, 0);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor);
- Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor);
+ done();
+ });
});
it('should call the success callback when the interval header is -1', (done) => {
@@ -81,7 +100,7 @@ describe('Poll', () => {
Vue.http.interceptors.push(intervalInterceptor);
new Poll({
- resource: new ServiceMock('endpoint'),
+ resource: service,
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
@@ -90,10 +109,11 @@ describe('Poll', () => {
setTimeout(() => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
+
+ Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor);
+
done();
}, 0);
-
- Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor);
});
it('starts polling when http status is 200 and interval header is provided', (done) => {
@@ -103,26 +123,28 @@ describe('Poll', () => {
Vue.http.interceptors.push(pollInterceptor);
- const service = new ServiceMock('endpoint');
- spyOn(service, 'fetch').and.callThrough();
-
- new Poll({
+ const Polling = new Poll({
resource: service,
method: 'fetch',
data: { page: 1 },
successCallback: callbacks.success,
errorCallback: callbacks.error,
- }).makeRequest();
+ });
+
+ Polling.makeRequest();
+
+ waitForAllCallsToFinish(service, 2, () => {
+ Polling.stop();
- setTimeout(() => {
expect(service.fetch.calls.count()).toEqual(2);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
- done();
- }, 5);
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+
+ done();
+ });
});
describe('stop', () => {
@@ -133,9 +155,6 @@ describe('Poll', () => {
Vue.http.interceptors.push(pollInterceptor);
- const service = new ServiceMock('endpoint');
- spyOn(service, 'fetch').and.callThrough();
-
const Polling = new Poll({
resource: service,
method: 'fetch',
@@ -150,14 +169,15 @@ describe('Poll', () => {
Polling.makeRequest();
- setTimeout(() => {
+ waitForAllCallsToFinish(service, 1, () => {
expect(service.fetch.calls.count()).toEqual(1);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(Polling.stop).toHaveBeenCalled();
- done();
- }, 100);
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+
+ done();
+ });
});
});
@@ -169,10 +189,6 @@ describe('Poll', () => {
Vue.http.interceptors.push(pollInterceptor);
- const service = new ServiceMock('endpoint');
-
- spyOn(service, 'fetch').and.callThrough();
-
const Polling = new Poll({
resource: service,
method: 'fetch',
@@ -187,17 +203,22 @@ describe('Poll', () => {
});
spyOn(Polling, 'stop').and.callThrough();
+ spyOn(Polling, 'restart').and.callThrough();
Polling.makeRequest();
- setTimeout(() => {
+ waitForAllCallsToFinish(service, 2, () => {
+ Polling.stop();
+
expect(service.fetch.calls.count()).toEqual(2);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(Polling.stop).toHaveBeenCalled();
- done();
- }, 10);
+ expect(Polling.restart).toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index 4200e943121..daef9b93fa5 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -1,110 +1,108 @@
require('~/lib/utils/text_utility');
-(() => {
- describe('text_utility', () => {
- describe('gl.text.getTextWidth', () => {
- it('returns zero width when no text is passed', () => {
- expect(gl.text.getTextWidth('')).toBe(0);
- });
+describe('text_utility', () => {
+ describe('gl.text.getTextWidth', () => {
+ it('returns zero width when no text is passed', () => {
+ expect(gl.text.getTextWidth('')).toBe(0);
+ });
- it('returns zero width when no text is passed and font is passed', () => {
- expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
- });
+ it('returns zero width when no text is passed and font is passed', () => {
+ expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
+ });
- it('returns width when text is passed', () => {
- expect(gl.text.getTextWidth('foo') > 0).toBe(true);
- });
+ it('returns width when text is passed', () => {
+ expect(gl.text.getTextWidth('foo') > 0).toBe(true);
+ });
- it('returns bigger width when font is larger', () => {
- const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
- const regular = gl.text.getTextWidth('foo', '10px sans-serif');
- expect(largeFont > regular).toBe(true);
- });
+ it('returns bigger width when font is larger', () => {
+ const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
+ const regular = gl.text.getTextWidth('foo', '10px sans-serif');
+ expect(largeFont > regular).toBe(true);
});
+ });
- describe('gl.text.pluralize', () => {
- it('returns pluralized', () => {
- expect(gl.text.pluralize('test', 2)).toBe('tests');
- });
+ describe('gl.text.pluralize', () => {
+ it('returns pluralized', () => {
+ expect(gl.text.pluralize('test', 2)).toBe('tests');
+ });
- it('returns pluralized when count is 0', () => {
- expect(gl.text.pluralize('test', 0)).toBe('tests');
- });
+ it('returns pluralized when count is 0', () => {
+ expect(gl.text.pluralize('test', 0)).toBe('tests');
+ });
- it('does not return pluralized', () => {
- expect(gl.text.pluralize('test', 1)).toBe('test');
- });
+ it('does not return pluralized', () => {
+ expect(gl.text.pluralize('test', 1)).toBe('test');
});
+ });
- describe('gl.text.highCountTrim', () => {
- it('returns 99+ for count >= 100', () => {
- expect(gl.text.highCountTrim(105)).toBe('99+');
- expect(gl.text.highCountTrim(100)).toBe('99+');
- });
+ describe('gl.text.highCountTrim', () => {
+ it('returns 99+ for count >= 100', () => {
+ expect(gl.text.highCountTrim(105)).toBe('99+');
+ expect(gl.text.highCountTrim(100)).toBe('99+');
+ });
- it('returns exact number for count < 100', () => {
- expect(gl.text.highCountTrim(45)).toBe(45);
- });
+ it('returns exact number for count < 100', () => {
+ expect(gl.text.highCountTrim(45)).toBe(45);
});
+ });
- describe('gl.text.insertText', () => {
- let textArea;
+ describe('gl.text.insertText', () => {
+ let textArea;
- beforeAll(() => {
- textArea = document.createElement('textarea');
- document.querySelector('body').appendChild(textArea);
- });
+ beforeAll(() => {
+ textArea = document.createElement('textarea');
+ document.querySelector('body').appendChild(textArea);
+ });
- afterAll(() => {
- textArea.parentNode.removeChild(textArea);
- });
+ afterAll(() => {
+ textArea.parentNode.removeChild(textArea);
+ });
- describe('without selection', () => {
- it('inserts the tag on an empty line', () => {
- const initialValue = '';
+ describe('without selection', () => {
+ it('inserts the tag on an empty line', () => {
+ const initialValue = '';
- textArea.value = initialValue;
- textArea.selectionStart = 0;
- textArea.selectionEnd = 0;
+ textArea.value = initialValue;
+ textArea.selectionStart = 0;
+ textArea.selectionEnd = 0;
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+ gl.text.insertText(textArea, textArea.value, '*', null, '', false);
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
- it('inserts the tag on a new line if the current one is not empty', () => {
- const initialValue = 'some text';
+ it('inserts the tag on a new line if the current one is not empty', () => {
+ const initialValue = 'some text';
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+ gl.text.insertText(textArea, textArea.value, '*', null, '', false);
- expect(textArea.value).toEqual(`${initialValue}\n* `);
- });
+ expect(textArea.value).toEqual(`${initialValue}\n* `);
+ });
- it('inserts the tag on the same line if the current line only contains spaces', () => {
- const initialValue = ' ';
+ it('inserts the tag on the same line if the current line only contains spaces', () => {
+ const initialValue = ' ';
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+ gl.text.insertText(textArea, textArea.value, '*', null, '', false);
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
- it('inserts the tag on the same line if the current line only contains tabs', () => {
- const initialValue = '\t\t\t';
+ it('inserts the tag on the same line if the current line only contains tabs', () => {
+ const initialValue = '\t\t\t';
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+ gl.text.insertText(textArea, textArea.value, '*', null, '', false);
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
+ expect(textArea.value).toEqual(`${initialValue}* `);
});
});
});
-})();
+});
diff --git a/spec/javascripts/merged_buttons_spec.js b/spec/javascripts/merged_buttons_spec.js
new file mode 100644
index 00000000000..b5c5e60dd97
--- /dev/null
+++ b/spec/javascripts/merged_buttons_spec.js
@@ -0,0 +1,44 @@
+/* global MergedButtons */
+
+import '~/merged_buttons';
+
+describe('MergedButtons', () => {
+ const fixturesPath = 'merge_requests/merged_merge_request.html.raw';
+ preloadFixtures(fixturesPath);
+
+ beforeEach(() => {
+ loadFixtures(fixturesPath);
+ this.mergedButtons = new MergedButtons();
+ this.$removeBranchWidget = $('.remove_source_branch_widget:not(.failed)');
+ this.$removeBranchProgress = $('.remove_source_branch_in_progress');
+ this.$removeBranchFailed = $('.remove_source_branch_widget.failed');
+ this.$removeBranchButton = $('.remove_source_branch');
+ });
+
+ describe('removeSourceBranch', () => {
+ it('shows loader', () => {
+ $('.remove_source_branch').trigger('click');
+ expect(this.$removeBranchProgress).toBeVisible();
+ expect(this.$removeBranchWidget).not.toBeVisible();
+ });
+ });
+
+ describe('removeBranchSuccess', () => {
+ it('refreshes page when branch removed', () => {
+ spyOn(gl.utils, 'refreshCurrentPage').and.stub();
+ const response = { status: 200 };
+ this.$removeBranchButton.trigger('ajax:success', response, 'xhr');
+ expect(gl.utils.refreshCurrentPage).toHaveBeenCalled();
+ });
+ });
+
+ describe('removeBranchError', () => {
+ it('shows error message', () => {
+ const response = { status: 500 };
+ this.$removeBranchButton.trigger('ajax:error', response, 'xhr');
+ expect(this.$removeBranchFailed).toBeVisible();
+ expect(this.$removeBranchProgress).not.toBeVisible();
+ expect(this.$removeBranchWidget).not.toBeVisible();
+ });
+ });
+});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index d81a5bbb6a5..ca8ee04d955 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -72,5 +72,157 @@ require('~/lib/utils/text_utility');
expect(this.autoSizeSpy).toHaveBeenTriggered();
});
});
+
+ describe('renderNote', () => {
+ let notes;
+ let note;
+ let $notesList;
+
+ beforeEach(() => {
+ note = {
+ discussion_html: null,
+ valid: true,
+ html: '<div></div>',
+ };
+ $notesList = jasmine.createSpyObj('$notesList', ['find']);
+
+ notes = jasmine.createSpyObj('notes', [
+ 'refresh',
+ 'isNewNote',
+ 'collapseLongCommitList',
+ 'updateNotesCount',
+ ]);
+ notes.taskList = jasmine.createSpyObj('tasklist', ['init']);
+ notes.note_ids = [];
+
+ spyOn(window, '$').and.returnValue($notesList);
+ spyOn(gl.utils, 'localTimeAgo');
+ spyOn(Notes, 'animateAppendNote');
+ notes.isNewNote.and.returnValue(true);
+
+ Notes.prototype.renderNote.call(notes, note);
+ });
+
+ it('should query for the notes list', () => {
+ expect(window.$).toHaveBeenCalledWith('ul.main-notes-list');
+ });
+
+ it('should call .animateAppendNote', () => {
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
+ });
+ });
+
+ describe('renderDiscussionNote', () => {
+ let discussionContainer;
+ let note;
+ let notes;
+ let $form;
+ let row;
+
+ beforeEach(() => {
+ note = {
+ html: '<li></li>',
+ discussion_html: '<div></div>',
+ discussion_id: 1,
+ discussion_resolvable: false,
+ diff_discussion_html: false,
+ };
+ $form = jasmine.createSpyObj('$form', ['closest', 'find']);
+ row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']);
+
+ notes = jasmine.createSpyObj('notes', [
+ 'isNewNote',
+ 'isParallelView',
+ 'updateNotesCount',
+ ]);
+ notes.note_ids = [];
+
+ spyOn(gl.utils, 'localTimeAgo');
+ spyOn(Notes, 'animateAppendNote');
+ notes.isNewNote.and.returnValue(true);
+ notes.isParallelView.and.returnValue(false);
+ row.prevAll.and.returnValue(row);
+ row.first.and.returnValue(row);
+ row.find.and.returnValue(row);
+ });
+
+ describe('Discussion root note', () => {
+ let $notesList;
+ let body;
+
+ beforeEach(() => {
+ body = jasmine.createSpyObj('body', ['attr']);
+ discussionContainer = { length: 0 };
+
+ spyOn(window, '$').and.returnValues(discussionContainer, body, $notesList);
+ $form.closest.and.returnValues(row, $form);
+ $form.find.and.returnValues(discussionContainer);
+ body.attr.and.returnValue('');
+
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+ });
+
+ it('should query for the notes list', () => {
+ expect(window.$.calls.argsFor(2)).toEqual(['ul.main-notes-list']);
+ });
+
+ it('should call Notes.animateAppendNote', () => {
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $notesList);
+ });
+ });
+
+ describe('Discussion sub note', () => {
+ beforeEach(() => {
+ discussionContainer = { length: 1 };
+
+ spyOn(window, '$').and.returnValues(discussionContainer);
+ $form.closest.and.returnValues(row);
+
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+ });
+
+ it('should query foor the discussion container', () => {
+ expect(window.$).toHaveBeenCalledWith(`.notes[data-discussion-id="${note.discussion_id}"]`);
+ });
+
+ it('should call Notes.animateAppendNote', () => {
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer);
+ });
+ });
+ });
+
+ describe('animateAppendNote', () => {
+ let noteHTML;
+ let $note;
+ let $notesList;
+
+ beforeEach(() => {
+ noteHTML = '<div></div>';
+ $note = jasmine.createSpyObj('$note', ['addClass', 'renderGFM', 'removeClass']);
+ $notesList = jasmine.createSpyObj('$notesList', ['append']);
+
+ spyOn(window, '$').and.returnValue($note);
+ spyOn(window, 'setTimeout').and.callThrough();
+ $note.addClass.and.returnValue($note);
+ $note.renderGFM.and.returnValue($note);
+
+ Notes.animateAppendNote(noteHTML, $notesList);
+ });
+
+ it('should init the note jquery object', () => {
+ expect(window.$).toHaveBeenCalledWith(noteHTML);
+ });
+
+ it('should call addClass', () => {
+ expect($note.addClass).toHaveBeenCalledWith('fade-in');
+ });
+ it('should call renderGFM', () => {
+ expect($note.renderGFM).toHaveBeenCalledWith();
+ });
+
+ it('should append note to the notes list', () => {
+ expect($notesList.append).toHaveBeenCalledWith($note);
+ });
+ });
});
}).call(window);
diff --git a/spec/javascripts/vue_pipelines_index/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js
index bc8e504c413..28c9c7ab282 100644
--- a/spec/javascripts/vue_pipelines_index/async_button_spec.js
+++ b/spec/javascripts/pipelines/async_button_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import asyncButtonComp from '~/vue_pipelines_index/components/async_button';
+import asyncButtonComp from '~/pipelines/components/async_button.vue';
describe('Pipelines Async Button', () => {
let component;
diff --git a/spec/javascripts/vue_pipelines_index/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js
index 733337168dc..bb47a28d9fe 100644
--- a/spec/javascripts/vue_pipelines_index/empty_state_spec.js
+++ b/spec/javascripts/pipelines/empty_state_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import emptyStateComp from '~/vue_pipelines_index/components/empty_state';
+import emptyStateComp from '~/pipelines/components/empty_state.vue';
describe('Pipelines Empty State', () => {
let component;
diff --git a/spec/javascripts/vue_pipelines_index/error_state_spec.js b/spec/javascripts/pipelines/error_state_spec.js
index 524e018b1fa..f667d351f72 100644
--- a/spec/javascripts/vue_pipelines_index/error_state_spec.js
+++ b/spec/javascripts/pipelines/error_state_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import errorStateComp from '~/vue_pipelines_index/components/error_state';
+import errorStateComp from '~/pipelines/components/error_state.vue';
describe('Pipelines Error State', () => {
let component;
diff --git a/spec/javascripts/vue_pipelines_index/mock_data.js b/spec/javascripts/pipelines/mock_data.js
index 2365a662b9f..2365a662b9f 100644
--- a/spec/javascripts/vue_pipelines_index/mock_data.js
+++ b/spec/javascripts/pipelines/mock_data.js
diff --git a/spec/javascripts/vue_pipelines_index/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js
index 659c4854a56..601eebce38a 100644
--- a/spec/javascripts/vue_pipelines_index/nav_controls_spec.js
+++ b/spec/javascripts/pipelines/nav_controls_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import navControlsComp from '~/vue_pipelines_index/components/nav_controls';
+import navControlsComp from '~/pipelines/components/nav_controls';
describe('Pipelines Nav Controls', () => {
let NavControlsComponent;
diff --git a/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index 96a2a37b5f7..53931d67ad7 100644
--- a/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import pipelineUrlComp from '~/vue_pipelines_index/components/pipeline_url';
+import pipelineUrlComp from '~/pipelines/components/pipeline_url';
describe('Pipeline Url Component', () => {
let PipelineUrlComponent;
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js
index 0910df61915..c89dacbcd93 100644
--- a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
+++ b/spec/javascripts/pipelines/pipelines_actions_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import pipelinesActionsComp from '~/vue_pipelines_index/components/pipelines_actions';
+import pipelinesActionsComp from '~/pipelines/components/pipelines_actions';
describe('Pipelines Actions dropdown', () => {
let component;
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js b/spec/javascripts/pipelines/pipelines_artifacts_spec.js
index f7f49649c1c..9724b63d957 100644
--- a/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js
+++ b/spec/javascripts/pipelines/pipelines_artifacts_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import artifactsComp from '~/vue_pipelines_index/components/pipelines_artifacts';
+import artifactsComp from '~/pipelines/components/pipelines_artifacts';
describe('Pipelines Artifacts dropdown', () => {
let component;
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index 725f6cb2d7a..e9c05f74ce6 100644
--- a/spec/javascripts/vue_pipelines_index/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import pipelinesComp from '~/vue_pipelines_index/pipelines';
-import Store from '~/vue_pipelines_index/stores/pipelines_store';
+import pipelinesComp from '~/pipelines/pipelines';
+import Store from '~/pipelines/stores/pipelines_store';
import pipelinesData from './mock_data';
describe('Pipelines', () => {
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js b/spec/javascripts/pipelines/pipelines_store_spec.js
index 5c0934404bb..10ff0c6bb84 100644
--- a/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
+++ b/spec/javascripts/pipelines/pipelines_store_spec.js
@@ -1,4 +1,4 @@
-import PipelineStore from '~/vue_pipelines_index/stores/pipelines_store';
+import PipelineStore from '~/pipelines/stores/pipelines_store';
describe('Pipelines Store', () => {
let store;
diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js
new file mode 100644
index 00000000000..66b57a82363
--- /dev/null
+++ b/spec/javascripts/pipelines/stage_spec.js
@@ -0,0 +1,66 @@
+import Vue from 'vue';
+import { SUCCESS_SVG } from '~/ci_status_icons';
+import Stage from '~/pipelines/components/stage';
+
+function minify(string) {
+ return string.replace(/\s/g, '');
+}
+
+describe('Pipelines Stage', () => {
+ describe('data', () => {
+ let stageReturnValue;
+
+ beforeEach(() => {
+ stageReturnValue = Stage.data();
+ });
+
+ it('should return object with .builds and .spinner', () => {
+ expect(stageReturnValue).toEqual({
+ builds: '',
+ spinner: '<span class="fa fa-spinner fa-spin"></span>',
+ });
+ });
+ });
+
+ describe('computed', () => {
+ describe('svgHTML', function () {
+ let stage;
+ let svgHTML;
+
+ beforeEach(() => {
+ stage = { stage: { status: { icon: 'icon_status_success' } } };
+
+ svgHTML = Stage.computed.svgHTML.call(stage);
+ });
+
+ it("should return the correct icon for the stage's status", () => {
+ expect(svgHTML).toBe(SUCCESS_SVG);
+ });
+ });
+ });
+
+ describe('when mounted', () => {
+ let StageComponent;
+ let renderedComponent;
+ let stage;
+
+ beforeEach(() => {
+ stage = { status: { icon: 'icon_status_success' } };
+
+ StageComponent = Vue.extend(Stage);
+
+ renderedComponent = new StageComponent({
+ propsData: {
+ stage,
+ },
+ }).$mount();
+ });
+
+ it('should render the correct status svg', () => {
+ const minifiedComponent = minify(renderedComponent.$el.outerHTML);
+ const expectedSVG = minify(SUCCESS_SVG);
+
+ expect(minifiedComponent).toContain(expectedSVG);
+ });
+ });
+});
diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js
new file mode 100644
index 00000000000..9b8373df29e
--- /dev/null
+++ b/spec/javascripts/shortcuts_spec.js
@@ -0,0 +1,45 @@
+/* global Shortcuts */
+describe('Shortcuts', () => {
+ const fixtureName = 'issues/issue_with_comment.html.raw';
+ const createEvent = (type, target) => $.Event(type, {
+ target,
+ });
+
+ preloadFixtures(fixtureName);
+
+ describe('toggleMarkdownPreview', () => {
+ let sc;
+
+ beforeEach(() => {
+ loadFixtures(fixtureName);
+
+ spyOnEvent('.js-new-note-form .js-md-preview-button', 'focus');
+ spyOnEvent('.edit-note .js-md-preview-button', 'focus');
+
+ sc = new Shortcuts();
+ });
+
+ it('focuses preview button in form', () => {
+ sc.toggleMarkdownPreview(
+ 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) => {
+ document.querySelector('.js-note-edit').click();
+
+ setTimeout(() => {
+ sc.toggleMarkdownPreview(
+ 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');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js
index c0375ebc61c..28d0c7dcd99 100644
--- a/spec/javascripts/user_callout_spec.js
+++ b/spec/javascripts/user_callout_spec.js
@@ -14,7 +14,6 @@ describe('UserCallout', function () {
this.userCallout = new UserCallout();
this.closeButton = $('.js-close-callout.close');
this.userCalloutBtn = $('.js-close-callout:not(.close)');
- this.userCalloutContainer = $('.user-callout');
});
it('hides when user clicks on the dismiss-icon', (done) => {
diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
index 603b79a323c..600f3c123ed 100644
--- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb
+++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
@@ -5,9 +5,10 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
include FilterSpecHelper
let(:user) { create(:user) }
+ let(:context) { { current_user: user, issuable_state_filter_enabled: true } }
- def create_link(data)
- link_to('text', '', class: 'gfm has-tooltip', data: data)
+ def create_link(text, data)
+ link_to(text, '', class: 'gfm has-tooltip', data: data)
end
it 'ignores non-GFM links' do
@@ -19,8 +20,62 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
it 'ignores non-issuable links' do
project = create(:empty_project, :public)
- link = create_link(project: project, reference_type: 'issue')
- doc = filter(link, current_user: user)
+ link = create_link('text', project: project, reference_type: 'issue')
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq('text')
+ end
+
+ it 'ignores issuable links with empty content' do
+ issue = create(:issue, :closed)
+ link = create_link('', issue: issue.id, reference_type: 'issue')
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq('')
+ end
+
+ it 'ignores issuable links with custom anchor' do
+ issue = create(:issue, :closed)
+ link = create_link('something', issue: issue.id, reference_type: 'issue')
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq('something')
+ end
+
+ it 'ignores issuable links to specific comments' do
+ issue = create(:issue, :closed)
+ link = create_link("#{issue.to_reference} (comment 1)", issue: issue.id, reference_type: 'issue')
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq("#{issue.to_reference} (comment 1)")
+ end
+
+ it 'ignores merge request links to diffs tab' do
+ merge_request = create(:merge_request, :closed)
+ link = create_link(
+ "#{merge_request.to_reference} (diffs)",
+ merge_request: merge_request.id,
+ reference_type: 'merge_request'
+ )
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (diffs)")
+ end
+
+ it 'handles cross project references' do
+ issue = create(:issue, :closed)
+ project = create(:empty_project)
+ link = create_link(issue.to_reference(project), issue: issue.id, reference_type: 'issue')
+ doc = filter(link, context.merge(project: project))
+
+ expect(doc.css('a').last.text).to eq("#{issue.to_reference(project)} (closed)")
+ end
+
+ it 'does not append state when filter is not enabled' do
+ issue = create(:issue, :closed)
+ link = create_link('text', issue: issue.id, reference_type: 'issue')
+ context = { current_user: user }
+ doc = filter(link, context)
expect(doc.css('a').last.text).to eq('text')
end
@@ -28,68 +83,88 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
context 'for issue references' do
it 'ignores open issue references' do
issue = create(:issue)
- link = create_link(issue: issue.id, reference_type: 'issue')
- doc = filter(link, current_user: user)
+ link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue')
+ doc = filter(link, context)
- expect(doc.css('a').last.text).to eq('text')
+ expect(doc.css('a').last.text).to eq(issue.to_reference)
end
it 'ignores reopened issue references' do
- reopened_issue = create(:issue, :reopened)
- link = create_link(issue: reopened_issue.id, reference_type: 'issue')
- doc = filter(link, current_user: user)
+ issue = create(:issue, :reopened)
+ link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue')
+ doc = filter(link, context)
- expect(doc.css('a').last.text).to eq('text')
+ expect(doc.css('a').last.text).to eq(issue.to_reference)
end
- it 'appends [closed] to closed issue references' do
- closed_issue = create(:issue, :closed)
- link = create_link(issue: closed_issue.id, reference_type: 'issue')
- doc = filter(link, current_user: user)
+ it 'appends state to closed issue references' do
+ issue = create(:issue, :closed)
+ link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue')
+ doc = filter(link, context)
- expect(doc.css('a').last.text).to eq('text [closed]')
+ expect(doc.css('a').last.text).to eq("#{issue.to_reference} (closed)")
end
end
context 'for merge request references' do
it 'ignores open merge request references' do
- mr = create(:merge_request)
- link = create_link(merge_request: mr.id, reference_type: 'merge_request')
- doc = filter(link, current_user: user)
-
- expect(doc.css('a').last.text).to eq('text')
+ merge_request = create(:merge_request)
+ link = create_link(
+ merge_request.to_reference,
+ merge_request: merge_request.id,
+ reference_type: 'merge_request'
+ )
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq(merge_request.to_reference)
end
it 'ignores reopened merge request references' do
- mr = create(:merge_request, :reopened)
- link = create_link(merge_request: mr.id, reference_type: 'merge_request')
- doc = filter(link, current_user: user)
-
- expect(doc.css('a').last.text).to eq('text')
+ merge_request = create(:merge_request, :reopened)
+ link = create_link(
+ merge_request.to_reference,
+ merge_request: merge_request.id,
+ reference_type: 'merge_request'
+ )
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq(merge_request.to_reference)
end
it 'ignores locked merge request references' do
- mr = create(:merge_request, :locked)
- link = create_link(merge_request: mr.id, reference_type: 'merge_request')
- doc = filter(link, current_user: user)
-
- expect(doc.css('a').last.text).to eq('text')
+ merge_request = create(:merge_request, :locked)
+ link = create_link(
+ merge_request.to_reference,
+ merge_request: merge_request.id,
+ reference_type: 'merge_request'
+ )
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq(merge_request.to_reference)
end
- it 'appends [closed] to closed merge request references' do
- mr = create(:merge_request, :closed)
- link = create_link(merge_request: mr.id, reference_type: 'merge_request')
- doc = filter(link, current_user: user)
+ it 'appends state to closed merge request references' do
+ merge_request = create(:merge_request, :closed)
+ link = create_link(
+ merge_request.to_reference,
+ merge_request: merge_request.id,
+ reference_type: 'merge_request'
+ )
+ doc = filter(link, context)
- expect(doc.css('a').last.text).to eq('text [closed]')
+ expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (closed)")
end
- it 'appends [merged] to merged merge request references' do
- mr = create(:merge_request, :merged)
- link = create_link(merge_request: mr.id, reference_type: 'merge_request')
- doc = filter(link, current_user: user)
+ it 'appends state to merged merge request references' do
+ merge_request = create(:merge_request, :merged)
+ link = create_link(
+ merge_request.to_reference,
+ merge_request: merge_request.id,
+ reference_type: 'merge_request'
+ )
+ doc = filter(link, context)
- expect(doc.css('a').last.text).to eq('text [merged]')
+ expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (merged)")
end
end
end
diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb
index f85a5dcbd8b..9b8ecb201f3 100644
--- a/spec/lib/banzai/filter/plantuml_filter_spec.rb
+++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb
@@ -5,7 +5,7 @@ describe Banzai::Filter::PlantumlFilter, lib: true do
it 'should replace plantuml pre tag with img tag' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
- input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
+ input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
doc = filter(input)
@@ -14,8 +14,8 @@ describe Banzai::Filter::PlantumlFilter, lib: true do
it 'should not replace plantuml pre tag with img tag if disabled' do
stub_application_setting(plantuml_enabled: false)
- input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
- output = '<pre class="plantuml"><code>Bob -&gt; Sara : Hello</code><pre></pre></pre>'
+ input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
+ output = '<pre><code lang="plantuml">Bob -&gt; Sara : Hello</code></pre>'
doc = filter(input)
expect(doc.to_s).to eq output
@@ -23,7 +23,7 @@ describe Banzai::Filter::PlantumlFilter, lib: true do
it 'should not replace plantuml pre tag with img tag if url is invalid' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
- input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
+ input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
doc = filter(input)
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index 4817fcd031a..dd2674f9f20 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -4,13 +4,13 @@ describe Banzai::ObjectRenderer do
let(:project) { create(:empty_project) }
let(:user) { project.owner }
let(:renderer) { described_class.new(project, user, custom_value: 'value') }
- let(:object) { Note.new(note: 'hello', note_html: '<p>hello</p>') }
+ let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_VERSION) }
describe '#render' do
it 'renders and redacts an Array of objects' do
renderer.render([object], :note)
- expect(object.redacted_note_html).to eq '<p>hello</p>'
+ expect(object.redacted_note_html).to eq '<p dir="auto">hello</p>'
expect(object.user_visible_reference_count).to eq 0
end
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
index 6d2c141e18b..e6f2963193c 100644
--- a/spec/lib/banzai/redactor_spec.rb
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -42,6 +42,31 @@ describe Banzai::Redactor do
end
end
+ context 'when project is in pending delete' do
+ let!(:issue) { create(:issue, project: project) }
+ let(:redactor) { described_class.new(project, user) }
+
+ before do
+ project.update(pending_delete: true)
+ end
+
+ it 'redacts an issue attached' do
+ doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>")
+
+ redactor.redact([doc])
+
+ expect(doc.to_html).to eq('foo')
+ end
+
+ it 'redacts an external issue' do
+ doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>")
+
+ redactor.redact([doc])
+
+ expect(doc.to_html).to eq('foo')
+ end
+ end
+
context 'when reference visible to user' do
it 'does not redact an array of documents' do
doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>'
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
index a3141894c74..d5746107ee1 100644
--- a/spec/lib/banzai/reference_parser/base_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -114,8 +114,27 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
expect(hash).to eq({ link => user })
end
- it 'returns an empty Hash when the list of nodes is empty' do
- expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({})
+ it 'returns an empty Hash when entry does not exist in the database' do
+ link = double(:link)
+
+ expect(link).to receive(:has_attribute?).
+ with('data-user').
+ and_return(true)
+
+ expect(link).to receive(:attr).
+ with('data-user').
+ and_return('1')
+
+ nodes = [link]
+ bad_id = user.id + 100
+
+ expect(subject).to receive(:unique_attribute_values).
+ with(nodes, 'data-user').
+ and_return([bad_id.to_s])
+
+ hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user')
+
+ expect(hash).to eq({})
end
end
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index aaa6b12e67e..e6f8d2a1fed 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -1,73 +1,36 @@
require 'spec_helper'
describe Banzai::Renderer do
- def expect_render(project = :project)
- expected_context = { project: project }
- expect(renderer).to receive(:cacheless_render) { :html }.with(:markdown, expected_context)
- end
-
- def expect_cache_update
- expect(object).to receive(:update_column).with("field_html", :html)
- end
-
- def fake_object(*features)
- markdown = :markdown if features.include?(:markdown)
- html = :html if features.include?(:html)
-
- object = double(
- "object",
- banzai_render_context: { project: :project },
- field: markdown,
- field_html: html
- )
+ def fake_object(fresh:)
+ object = double('object')
- allow(object).to receive(:markdown_cache_field_for).with(:field).and_return("field_html")
- allow(object).to receive(:new_record?).and_return(features.include?(:new))
- allow(object).to receive(:destroyed?).and_return(features.include?(:destroyed))
+ allow(object).to receive(:cached_html_up_to_date?).with(:field).and_return(fresh)
+ allow(object).to receive(:cached_html_for).with(:field).and_return('field_html')
object
end
- describe "#render_field" do
+ describe '#render_field' do
let(:renderer) { Banzai::Renderer }
- let(:subject) { renderer.render_field(object, :field) }
+ subject { renderer.render_field(object, :field) }
- context "with an empty cache" do
- let(:object) { fake_object(:markdown) }
- it "caches and returns the result" do
- expect_render
- expect_cache_update
- expect(subject).to eq(:html)
- end
- end
+ context 'with a stale cache' do
+ let(:object) { fake_object(fresh: false) }
- context "with a filled cache" do
- let(:object) { fake_object(:markdown, :html) }
+ it 'caches and returns the result' do
+ expect(object).to receive(:refresh_markdown_cache!).with(do_update: true)
- it "uses the cache" do
- expect_render.never
- expect_cache_update.never
- should eq(:html)
+ is_expected.to eq('field_html')
end
end
- context "new object" do
- let(:object) { fake_object(:new, :markdown) }
-
- it "doesn't cache the result" do
- expect_render
- expect_cache_update.never
- expect(subject).to eq(:html)
- end
- end
+ context 'with an up-to-date cache' do
+ let(:object) { fake_object(fresh: true) }
- context "destroyed object" do
- let(:object) { fake_object(:destroyed, :markdown) }
+ it 'uses the cache' do
+ expect(object).to receive(:refresh_markdown_cache!).never
- it "doesn't cache the result" do
- expect_render
- expect_cache_update.never
- expect(subject).to eq(:html)
+ is_expected.to eq('field_html')
end
end
end
diff --git a/spec/lib/container_registry/path_spec.rb b/spec/lib/container_registry/path_spec.rb
index b9c4572c269..c2bcb54210b 100644
--- a/spec/lib/container_registry/path_spec.rb
+++ b/spec/lib/container_registry/path_spec.rb
@@ -33,10 +33,20 @@ describe ContainerRegistry::Path do
end
describe '#to_s' do
- let(:path) { 'some/image' }
+ context 'when path does not have uppercase characters' do
+ let(:path) { 'some/image' }
- it 'return a string with a repository path' do
- expect(subject.to_s).to eq path
+ it 'return a string with a repository path' do
+ expect(subject.to_s).to eq 'some/image'
+ end
+ end
+
+ context 'when path has uppercase characters' do
+ let(:path) { 'SoMe/ImAgE' }
+
+ it 'return a string with a repository path' do
+ expect(subject.to_s).to eq 'some/image'
+ end
end
end
@@ -70,6 +80,12 @@ describe ContainerRegistry::Path do
it { is_expected.to be_valid }
end
+
+ context 'when path contains uppercase letters' do
+ let(:path) { 'Some/Registry' }
+
+ it { is_expected.to be_valid }
+ end
end
describe '#has_repository?' do
@@ -173,15 +189,10 @@ describe ContainerRegistry::Path do
end
context 'when project exists' do
- let(:group) { create(:group, path: 'some_group') }
-
- let(:project) do
- create(:empty_project, group: group, name: 'some_project')
- end
+ let(:group) { create(:group, path: 'Some_Group') }
before do
- allow(path).to receive(:repository_project)
- .and_return(project)
+ create(:empty_project, group: group, name: 'some_project')
end
context 'when project path equal repository path' do
@@ -209,4 +220,27 @@ describe ContainerRegistry::Path do
end
end
end
+
+ describe '#project_path' do
+ context 'when project does not exist' do
+ let(:path) { 'some/name' }
+
+ it 'returns nil' do
+ expect(subject.project_path).to be_nil
+ end
+ end
+
+ context 'when project with uppercase characters in path exists' do
+ let(:path) { 'somegroup/myproject/my/image' }
+ let(:group) { create(:group, path: 'SomeGroup') }
+
+ before do
+ create(:empty_project, group: group, name: 'MyProject')
+ end
+
+ it 'returns downcased project path' do
+ expect(subject.project_path).to eq 'somegroup/myproject'
+ end
+ end
+ end
end
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index bc1912d8e6c..f8fffbdca41 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -50,6 +50,13 @@ describe ContainerRegistry::Tag do
end
end
+ describe '#location' do
+ it 'returns a full location of the tag' do
+ expect(tag.location)
+ .to eq 'registry.gitlab/group/test:tag'
+ end
+ end
+
context 'manifest processing' do
context 'schema v1' do
before do
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index f1a1a71c528..40ac5a3ed37 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -17,12 +17,12 @@ describe Gitlab::Ci::Trace::Stream do
describe '#limit' do
let(:stream) do
described_class.new do
- StringIO.new("12345678")
+ StringIO.new((1..8).to_a.join("\n"))
end
end
- it 'if size is larger we start from beggining' do
- stream.limit(10)
+ it 'if size is larger we start from beginning' do
+ stream.limit(20)
expect(stream.tell).to eq(0)
end
@@ -30,17 +30,61 @@ describe Gitlab::Ci::Trace::Stream do
it 'if size is smaller we start from the end' do
stream.limit(2)
- expect(stream.tell).to eq(6)
+ expect(stream.raw).to eq("8")
+ end
+
+ context 'when the trace contains ANSI sequence and Unicode' do
+ let(:stream) do
+ described_class.new do
+ File.open(expand_fixture_path('trace/ansi-sequence-and-unicode'))
+ end
+ end
+
+ it 'forwards to the next linefeed, case 1' do
+ stream.limit(7)
+
+ result = stream.raw
+
+ expect(result).to eq('')
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
+
+ it 'forwards to the next linefeed, case 2' do
+ stream.limit(29)
+
+ result = stream.raw
+
+ expect(result).to eq("\e[01;32m許功蓋\e[0m\n")
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
+
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796
+ it 'reads in binary, output as Encoding.default_external' do
+ stream.limit(52)
+
+ result = stream.html
+
+ expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>")
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
end
end
describe '#append' do
+ let(:tempfile) { Tempfile.new }
+
let(:stream) do
described_class.new do
- StringIO.new("12345678")
+ tempfile.write("12345678")
+ tempfile.rewind
+ tempfile
end
end
+ after do
+ tempfile.unlink
+ end
+
it "truncates and append content" do
stream.append("89", 4)
stream.seek(0)
@@ -48,6 +92,17 @@ describe Gitlab::Ci::Trace::Stream do
expect(stream.size).to eq(6)
expect(stream.raw).to eq("123489")
end
+
+ it 'appends in binary mode' do
+ '😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset|
+ stream.append(byte, offset)
+ end
+
+ stream.seek(0)
+
+ expect(stream.size).to eq(4)
+ expect(stream.raw).to eq('😺')
+ end
end
describe '#set' do
@@ -167,7 +222,7 @@ describe Gitlab::Ci::Trace::Stream do
let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
let(:regex) { '\(\d+.\d+\%\) covered' }
- it { is_expected.to eq(98.29) }
+ it { is_expected.to eq("98.29") }
end
context 'valid content & bad regex' do
@@ -188,14 +243,14 @@ describe Gitlab::Ci::Trace::Stream do
let(:data) { ' (98.39%) covered. (98.29%) covered' }
let(:regex) { '\(\d+.\d+\%\) covered' }
- it { is_expected.to eq(98.29) }
+ it { is_expected.to eq("98.29") }
end
context 'using a regex capture' do
let(:data) { 'TOTAL 9926 3489 65%' }
let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' }
- it { is_expected.to eq(65) }
+ it { is_expected.to eq("65") }
end
end
end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 69e8dc9220d..9cb0b62590a 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -40,12 +40,24 @@ describe Gitlab::Ci::Trace do
describe '#extract_coverage' do
let(:regex) { '\(\d+.\d+\%\) covered' }
- before do
- trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
+ context 'matching coverage' do
+ before do
+ trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
+ end
+
+ it "returns valid coverage" do
+ expect(trace.extract_coverage(regex)).to eq("98.29")
+ end
end
- it "returns valid coverage" do
- expect(trace.extract_coverage(regex)).to eq(98.29)
+ context 'no coverage' do
+ before do
+ trace.set('No coverage')
+ end
+
+ it 'returs nil' do
+ expect(trace.extract_coverage(regex)).to be_nil
+ end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 4ac79454647..a044b871730 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -175,6 +175,50 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
+ describe '#true_value' do
+ context 'using PostgreSQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.true_value).to eq("'t'")
+ end
+ end
+
+ context 'using MySQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.true_value).to eq(1)
+ end
+ end
+ end
+
+ describe '#false_value' do
+ context 'using PostgreSQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.false_value).to eq("'f'")
+ end
+ end
+
+ context 'using MySQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.false_value).to eq(0)
+ end
+ end
+ end
+
describe '#update_column_in_batches' do
before do
create_list(:empty_project, 5)
@@ -294,4 +338,392 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
end
+
+ describe '#rename_column_concurrently' do
+ context 'in a transaction' do
+ it 'raises RuntimeError' do
+ allow(model).to receive(:transaction_open?).and_return(true)
+
+ expect { model.rename_column_concurrently(:users, :old, :new) }.
+ to raise_error(RuntimeError)
+ end
+ end
+
+ context 'outside a transaction' do
+ let(:old_column) do
+ double(:column,
+ type: :integer,
+ limit: 8,
+ default: 0,
+ null: false,
+ precision: 5,
+ scale: 1)
+ end
+
+ let(:trigger_name) { model.rename_trigger_name(:users, :old, :new) }
+
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ allow(model).to receive(:column_for).and_return(old_column)
+
+ # Since MySQL and PostgreSQL use different quoting styles we'll just
+ # stub the methods used for this to make testing easier.
+ allow(model).to receive(:quote_column_name) { |name| name.to_s }
+ allow(model).to receive(:quote_table_name) { |name| name.to_s }
+ end
+
+ context 'using MySQL' do
+ it 'renames a column concurrently' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(model).to receive(:install_rename_triggers_for_mysql).
+ with(trigger_name, 'users', 'old', 'new')
+
+ expect(model).to receive(:add_column).
+ with(:users, :new, :integer,
+ limit: old_column.limit,
+ default: old_column.default,
+ null: old_column.null,
+ precision: old_column.precision,
+ scale: old_column.scale)
+
+ expect(model).to receive(:update_column_in_batches)
+
+ expect(model).to receive(:copy_indexes).with(:users, :old, :new)
+ expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
+
+ model.rename_column_concurrently(:users, :old, :new)
+ end
+ end
+
+ context 'using PostgreSQL' do
+ it 'renames a column concurrently' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(model).to receive(:install_rename_triggers_for_postgresql).
+ with(trigger_name, 'users', 'old', 'new')
+
+ expect(model).to receive(:add_column).
+ with(:users, :new, :integer,
+ limit: old_column.limit,
+ default: old_column.default,
+ null: old_column.null,
+ precision: old_column.precision,
+ scale: old_column.scale)
+
+ expect(model).to receive(:update_column_in_batches)
+
+ expect(model).to receive(:copy_indexes).with(:users, :old, :new)
+ expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
+
+ model.rename_column_concurrently(:users, :old, :new)
+ end
+ end
+ end
+ end
+
+ describe '#cleanup_concurrent_column_rename' do
+ it 'cleans up the renaming procedure for PostgreSQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(model).to receive(:remove_rename_triggers_for_postgresql).
+ with(:users, /trigger_.{12}/)
+
+ expect(model).to receive(:remove_column).with(:users, :old)
+
+ model.cleanup_concurrent_column_rename(:users, :old, :new)
+ end
+
+ it 'cleans up the renaming procedure for MySQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(model).to receive(:remove_rename_triggers_for_mysql).
+ with(/trigger_.{12}/)
+
+ expect(model).to receive(:remove_column).with(:users, :old)
+
+ model.cleanup_concurrent_column_rename(:users, :old, :new)
+ end
+ end
+
+ describe '#change_column_type_concurrently' do
+ it 'changes the column type' do
+ expect(model).to receive(:rename_column_concurrently).
+ with('users', 'username', 'username_for_type_change', type: :text)
+
+ model.change_column_type_concurrently('users', 'username', :text)
+ end
+ end
+
+ describe '#cleanup_concurrent_column_type_change' do
+ it 'cleans up the type changing procedure' do
+ expect(model).to receive(:cleanup_concurrent_column_rename).
+ with('users', 'username', 'username_for_type_change')
+
+ expect(model).to receive(:rename_column).
+ with('users', 'username_for_type_change', 'username')
+
+ model.cleanup_concurrent_column_type_change('users', 'username')
+ end
+ end
+
+ describe '#install_rename_triggers_for_postgresql' do
+ it 'installs the triggers for PostgreSQL' do
+ expect(model).to receive(:execute).
+ with(/CREATE OR REPLACE FUNCTION foo()/m)
+
+ expect(model).to receive(:execute).
+ with(/CREATE TRIGGER foo/m)
+
+ model.install_rename_triggers_for_postgresql('foo', :users, :old, :new)
+ end
+ end
+
+ describe '#install_rename_triggers_for_mysql' do
+ it 'installs the triggers for MySQL' do
+ expect(model).to receive(:execute).
+ with(/CREATE TRIGGER foo_insert.+ON users/m)
+
+ expect(model).to receive(:execute).
+ with(/CREATE TRIGGER foo_update.+ON users/m)
+
+ model.install_rename_triggers_for_mysql('foo', :users, :old, :new)
+ end
+ end
+
+ describe '#remove_rename_triggers_for_postgresql' do
+ it 'removes the function and trigger' do
+ expect(model).to receive(:execute).with('DROP TRIGGER foo ON bar')
+ expect(model).to receive(:execute).with('DROP FUNCTION foo()')
+
+ model.remove_rename_triggers_for_postgresql('bar', 'foo')
+ end
+ end
+
+ describe '#remove_rename_triggers_for_mysql' do
+ it 'removes the triggers' do
+ expect(model).to receive(:execute).with('DROP TRIGGER foo_insert')
+ expect(model).to receive(:execute).with('DROP TRIGGER foo_update')
+
+ model.remove_rename_triggers_for_mysql('foo')
+ end
+ end
+
+ describe '#rename_trigger_name' do
+ it 'returns a String' do
+ expect(model.rename_trigger_name(:users, :foo, :bar)).
+ to match(/trigger_.{12}/)
+ end
+ end
+
+ describe '#indexes_for' do
+ it 'returns the indexes for a column' do
+ idx1 = double(:idx, columns: %w(project_id))
+ idx2 = double(:idx, columns: %w(user_id))
+
+ allow(model).to receive(:indexes).with('table').and_return([idx1, idx2])
+
+ expect(model.indexes_for('table', :user_id)).to eq([idx2])
+ end
+ end
+
+ describe '#foreign_keys_for' do
+ it 'returns the foreign keys for a column' do
+ fk1 = double(:fk, column: 'project_id')
+ fk2 = double(:fk, column: 'user_id')
+
+ allow(model).to receive(:foreign_keys).with('table').and_return([fk1, fk2])
+
+ expect(model.foreign_keys_for('table', :user_id)).to eq([fk2])
+ end
+ end
+
+ describe '#copy_indexes' do
+ context 'using a regular index using a single column' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ using: nil,
+ where: nil,
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [])
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using a regular index with multiple columns' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id foobar),
+ name: 'index_on_issues_project_id_foobar',
+ using: nil,
+ where: nil,
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id foobar),
+ unique: false,
+ name: 'index_on_issues_gl_project_id_foobar',
+ length: [],
+ order: [])
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using an index with a WHERE clause' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ using: nil,
+ where: 'foo',
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [],
+ where: 'foo')
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using an index with a USING clause' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ where: nil,
+ using: 'foo',
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [],
+ using: 'foo')
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using an index with custom operator classes' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ using: nil,
+ where: nil,
+ opclasses: { 'project_id' => 'bar' },
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [],
+ opclasses: { 'gl_project_id' => 'bar' })
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ describe 'using an index of which the name does not contain the source column' do
+ it 'raises RuntimeError' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_foobar_index',
+ using: nil,
+ where: nil,
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect { model.copy_indexes(:issues, :project_id, :gl_project_id) }.
+ to raise_error(RuntimeError)
+ end
+ end
+ end
+
+ describe '#copy_foreign_keys' do
+ it 'copies foreign keys from one column to another' do
+ fk = double(:fk,
+ from_table: 'issues',
+ to_table: 'projects',
+ on_delete: :cascade)
+
+ allow(model).to receive(:foreign_keys_for).with(:issues, :project_id).
+ and_return([fk])
+
+ expect(model).to receive(:add_concurrent_foreign_key).
+ with('issues', 'projects', column: :gl_project_id, on_delete: :cascade)
+
+ model.copy_foreign_keys(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ describe '#column_for' do
+ it 'returns a column object for an existing column' do
+ column = model.column_for(:users, :id)
+
+ expect(column.name).to eq('id')
+ end
+
+ it 'returns nil when a column does not exist' do
+ expect(model.column_for(:users, :kittens)).to be_nil
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/multi_threaded_migration_spec.rb b/spec/lib/gitlab/database/multi_threaded_migration_spec.rb
new file mode 100644
index 00000000000..6c45f13bb5a
--- /dev/null
+++ b/spec/lib/gitlab/database/multi_threaded_migration_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Database::MultiThreadedMigration do
+ let(:migration) do
+ Class.new { include Gitlab::Database::MultiThreadedMigration }.new
+ end
+
+ describe '#connection' do
+ after do
+ Thread.current[described_class::MULTI_THREAD_AR_CONNECTION] = nil
+ end
+
+ it 'returns the thread-local connection if present' do
+ Thread.current[described_class::MULTI_THREAD_AR_CONNECTION] = 10
+
+ expect(migration.connection).to eq(10)
+ end
+
+ it 'returns the global connection if no thread-local connection was set' do
+ expect(migration.connection).to eq(ActiveRecord::Base.connection)
+ end
+ end
+
+ describe '#with_multiple_threads' do
+ it 'starts multiple threads and yields the supplied block in every thread' do
+ output = Queue.new
+
+ migration.with_multiple_threads(2) do
+ output << migration.connection.execute('SELECT 1')
+ end
+
+ expect(output.size).to eq(2)
+ end
+
+ it 'joins the threads when the join parameter is set' do
+ expect_any_instance_of(Thread).to receive(:join).and_call_original
+
+ migration.with_multiple_threads(1) { }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 4ce4e6e1034..9b1d66a1b1c 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -150,13 +150,13 @@ describe Gitlab::Database, lib: true do
it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true)
- expect(MigrationTest.new.true_value).to eq "'t'"
+ expect(described_class.true_value).to eq "'t'"
end
it 'returns correct value for MySQL' do
expect(described_class).to receive(:postgresql?).and_return(false)
- expect(MigrationTest.new.true_value).to eq 1
+ expect(described_class.true_value).to eq 1
end
end
@@ -164,13 +164,13 @@ describe Gitlab::Database, lib: true do
it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true)
- expect(MigrationTest.new.false_value).to eq "'f'"
+ expect(described_class.false_value).to eq "'f'"
end
it 'returns correct value for MySQL' do
expect(described_class).to receive(:postgresql?).and_return(false)
- expect(MigrationTest.new.false_value).to eq 0
+ expect(described_class.false_value).to eq 0
end
end
end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index 994995b57b8..c166f83664a 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -100,7 +100,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
project,
current_user,
start_branch: branch_name,
- target_branch: branch_name,
+ branch_name: branch_name,
commit_message: "Create file",
file_path: file_name,
file_content: content
@@ -113,7 +113,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
project,
current_user,
start_branch: branch_name,
- target_branch: branch_name,
+ branch_name: branch_name,
commit_message: "Update file",
file_path: file_name,
file_content: content
@@ -122,11 +122,11 @@ describe Gitlab::Diff::PositionTracer, lib: true do
end
def delete_file(branch_name, file_name)
- Files::DestroyService.new(
+ Files::DeleteService.new(
project,
current_user,
start_branch: branch_name,
- target_branch: branch_name,
+ branch_name: branch_name,
commit_message: "Delete file",
file_path: file_name
).execute
diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb
new file mode 100644
index 00000000000..f3dacb4ef04
--- /dev/null
+++ b/spec/lib/gitlab/etag_caching/router_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+
+describe Gitlab::EtagCaching::Router do
+ it 'matches issue notes endpoint' do
+ env = build_env(
+ '/my-group/and-subgroup/here-comes-the-project/noteable/issue/1/notes'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'issue_notes'
+ end
+
+ it 'matches issue title endpoint' do
+ env = build_env(
+ '/my-group/my-project/issues/123/rendered_title'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'issue_title'
+ end
+
+ it 'matches project pipelines endpoint' do
+ env = build_env(
+ '/my-group/my-project/pipelines.json'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'project_pipelines'
+ end
+
+ it 'matches commit pipelines endpoint' do
+ env = build_env(
+ '/my-group/my-project/commit/aa8260d253a53f73f6c26c734c72fdd600f6e6d4/pipelines.json'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'commit_pipelines'
+ end
+
+ it 'matches new merge request pipelines endpoint' do
+ env = build_env(
+ '/my-group/my-project/merge_requests/new.json'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'new_merge_request_pipelines'
+ end
+
+ it 'matches merge request pipelines endpoint' do
+ env = build_env(
+ '/my-group/my-project/merge_requests/234/pipelines.json'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'merge_request_pipelines'
+ end
+
+ it 'does not match blob with confusing name' do
+ env = build_env(
+ '/my-group/my-project/blob/master/pipelines.json'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_blank
+ end
+
+ def build_env(path)
+ { 'PATH_INFO' => path }
+ end
+end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 3f494257545..e6a07a58d73 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -234,7 +234,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
it { expect(blob.lfs_pointer?).to eq(true) }
it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") }
- it { expect(blob.lfs_size).to eq("19548") }
+ it { expect(blob.lfs_size).to eq(19548) }
it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") }
it { expect(blob.name).to eq("image.jpg") }
it { expect(blob.path).to eq("files/lfs/image.jpg") }
@@ -273,7 +273,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
it { expect(blob.lfs_pointer?).to eq(false) }
it { expect(blob.lfs_oid).to eq(nil) }
- it { expect(blob.lfs_size).to eq("1575078") }
+ it { expect(blob.lfs_size).to eq(1575078) }
it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") }
it { expect(blob.name).to eq("picture-invalid.png") }
it { expect(blob.path).to eq("files/lfs/picture-invalid.png") }
diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/git/encoding_helper_spec.rb
index 27bcc241b82..f6ac7b23d1d 100644
--- a/spec/lib/gitlab/git/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/git/encoding_helper_spec.rb
@@ -56,6 +56,10 @@ describe Gitlab::Git::EncodingHelper do
expect(r.encoding.name).to eq('UTF-8')
end
end
+
+ it 'returns empty string on conversion errors' do
+ expect { ext_class.encode_utf8('') }.not_to raise_error(ArgumentError)
+ end
end
describe '#clean' do
diff --git a/spec/lib/gitlab/git/env_spec.rb b/spec/lib/gitlab/git/env_spec.rb
new file mode 100644
index 00000000000..d9df99bfe05
--- /dev/null
+++ b/spec/lib/gitlab/git/env_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Env do
+ describe "#set" do
+ context 'with RequestStore.store disabled' do
+ before do
+ allow(RequestStore).to receive(:active?).and_return(false)
+ end
+
+ it 'does not store anything' do
+ described_class.set(GIT_OBJECT_DIRECTORY: 'foo')
+
+ expect(described_class.all).to be_empty
+ end
+ end
+
+ context 'with RequestStore.store enabled' do
+ before do
+ allow(RequestStore).to receive(:active?).and_return(true)
+ end
+
+ it 'whitelist some `GIT_*` variables and stores them using RequestStore' do
+ described_class.set(
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar',
+ GIT_EXEC_PATH: 'baz',
+ PATH: '~/.bin:/bin')
+
+ expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
+ expect(described_class[:GIT_ALTERNATE_OBJECT_DIRECTORIES]).to eq('bar')
+ expect(described_class[:GIT_EXEC_PATH]).to be_nil
+ expect(described_class[:bar]).to be_nil
+ end
+ end
+ end
+
+ describe "#all" do
+ context 'with RequestStore.store enabled' do
+ before do
+ allow(RequestStore).to receive(:active?).and_return(true)
+ described_class.set(
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar')
+ end
+
+ it 'returns an env hash' do
+ expect(described_class.all).to eq({
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ })
+ end
+ end
+ end
+
+ describe "#[]" do
+ context 'with RequestStore.store enabled' do
+ before do
+ allow(RequestStore).to receive(:active?).and_return(true)
+ end
+
+ before do
+ described_class.set(
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar')
+ end
+
+ it 'returns a stored value for an existing key' do
+ expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
+ end
+
+ it 'returns nil for an non-existing key' do
+ expect(described_class[:foo]).to be_nil
+ end
+ end
+ end
+
+ describe 'thread-safety' do
+ context 'with RequestStore.store enabled' do
+ before do
+ allow(RequestStore).to receive(:active?).and_return(true)
+ described_class.set(GIT_OBJECT_DIRECTORY: 'foo')
+ end
+
+ it 'is thread-safe' do
+ another_thread = Thread.new do
+ described_class.set(GIT_OBJECT_DIRECTORY: 'bar')
+
+ Thread.stop
+ described_class[:GIT_OBJECT_DIRECTORY]
+ end
+
+ # Ensure another_thread runs first
+ sleep 0.1 until another_thread.stop?
+
+ expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
+
+ another_thread.run
+ expect(another_thread.value).to eq('bar')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb
index 07d71f6777d..21b71654251 100644
--- a/spec/lib/gitlab/git/index_spec.rb
+++ b/spec/lib/gitlab/git/index_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::Git::Index, seed_helper: true do
end
it 'raises an error' do
- expect { index.create(options) }.to raise_error('Filename already exists')
+ expect { index.create(options) }.to raise_error('A file with this name already exists')
end
end
@@ -89,7 +89,7 @@ describe Gitlab::Git::Index, seed_helper: true do
end
it 'raises an error' do
- expect { index.create_dir(options) }.to raise_error('Directory already exists as a file')
+ expect { index.create_dir(options) }.to raise_error('A file with this name already exists')
end
end
@@ -99,7 +99,7 @@ describe Gitlab::Git::Index, seed_helper: true do
end
it 'raises an error' do
- expect { index.create_dir(options) }.to raise_error('Directory already exists')
+ expect { index.create_dir(options) }.to raise_error('A directory with this name already exists')
end
end
end
@@ -118,7 +118,7 @@ describe Gitlab::Git::Index, seed_helper: true do
end
it 'raises an error' do
- expect { index.update(options) }.to raise_error("File doesn't exist")
+ expect { index.update(options) }.to raise_error("A file with this name doesn't exist")
end
end
@@ -156,7 +156,15 @@ describe Gitlab::Git::Index, seed_helper: true do
it 'raises an error' do
options[:previous_path] = 'documents/story.txt'
- expect { index.move(options) }.to raise_error("File doesn't exist")
+ expect { index.move(options) }.to raise_error("A file with this name doesn't exist")
+ end
+ end
+
+ context 'when a file at the new path already exists' do
+ it 'raises an error' do
+ options[:file_path] = 'CHANGELOG'
+
+ expect { index.move(options) }.to raise_error("A file with this name already exists")
end
end
@@ -203,7 +211,7 @@ describe Gitlab::Git::Index, seed_helper: true do
end
it 'raises an error' do
- expect { index.delete(options) }.to raise_error("File doesn't exist")
+ expect { index.delete(options) }.to raise_error("A file with this name doesn't exist")
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 7e8bb796e03..3d6d7292b42 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -24,18 +24,49 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- context 'with gitaly enabled' do
- before { stub_gitaly }
+ # TODO: Uncomment when feature is reenabled
+ # context 'with gitaly enabled' do
+ # before { stub_gitaly }
+ #
+ # it 'gets the branch name from GitalyClient' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
+ # repository.root_ref
+ # end
+ #
+ # it 'wraps GRPC exceptions' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name).
+ # and_raise(GRPC::Unknown)
+ # expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError)
+ # end
+ # end
+ end
- it 'gets the branch name from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
- repository.root_ref
+ describe "#rugged" do
+ context 'with no Git env stored' do
+ before do
+ expect(Gitlab::Git::Env).to receive(:all).and_return({})
+ end
+
+ it "whitelist some variables and pass them via the alternates keyword argument" do
+ expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: [])
+
+ repository.rugged
end
+ end
- it 'wraps GRPC exceptions' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name).
- and_raise(GRPC::Unknown)
- expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError)
+ context 'with some Git env stored' do
+ before do
+ expect(Gitlab::Git::Env).to receive(:all).and_return({
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar',
+ 'GIT_OTHER' => 'another_env'
+ })
+ end
+
+ it "whitelist some variables and pass them via the alternates keyword argument" do
+ expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: %w[foo bar])
+
+ repository.rugged
end
end
end
@@ -82,20 +113,21 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to include("master") }
it { is_expected.not_to include("branch-from-space") }
- context 'with gitaly enabled' do
- before { stub_gitaly }
-
- it 'gets the branch names from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
- subject
- end
-
- it 'wraps GRPC exceptions' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names).
- and_raise(GRPC::Unknown)
- expect { subject }.to raise_error(Gitlab::Git::CommandError)
- end
- end
+ # TODO: Uncomment when feature is reenabled
+ # context 'with gitaly enabled' do
+ # before { stub_gitaly }
+ #
+ # it 'gets the branch names from GitalyClient' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
+ # subject
+ # end
+ #
+ # it 'wraps GRPC exceptions' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names).
+ # and_raise(GRPC::Unknown)
+ # expect { subject }.to raise_error(Gitlab::Git::CommandError)
+ # end
+ # end
end
describe '#tag_names' do
@@ -113,20 +145,21 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to include("v1.0.0") }
it { is_expected.not_to include("v5.0.0") }
- context 'with gitaly enabled' do
- before { stub_gitaly }
-
- it 'gets the tag names from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
- subject
- end
-
- it 'wraps GRPC exceptions' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names).
- and_raise(GRPC::Unknown)
- expect { subject }.to raise_error(Gitlab::Git::CommandError)
- end
- end
+ # TODO: Uncomment when feature is reenabled
+ # context 'with gitaly enabled' do
+ # before { stub_gitaly }
+ #
+ # it 'gets the tag names from GitalyClient' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
+ # subject
+ # end
+ #
+ # it 'wraps GRPC exceptions' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names).
+ # and_raise(GRPC::Unknown)
+ # expect { subject }.to raise_error(Gitlab::Git::CommandError)
+ # end
+ # end
end
shared_examples 'archive check' do |extenstion|
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index d48629a296d..78894ba9409 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -3,58 +3,54 @@ require 'spec_helper'
describe Gitlab::Git::RevList, lib: true do
let(:project) { create(:project, :repository) }
- context "validations" do
- described_class::ALLOWED_VARIABLES.each do |var|
- context var do
- it "accepts values starting with the project repo path" do
- env = { var => "#{project.repository.path_to_repo}/objects" }
- rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
-
- expect(rev_list).to be_valid
- end
-
- it "rejects values starting not with the project repo path" do
- env = { var => "/some/other/path" }
- rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
-
- expect(rev_list).not_to be_valid
- end
-
- it "rejects values containing the project repo path but not starting with it" do
- env = { var => "/some/other/path/#{project.repository.path_to_repo}" }
- rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
-
- expect(rev_list).not_to be_valid
- end
-
- it "ignores nil values" do
- env = { var => nil }
- rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
-
- expect(rev_list).to be_valid
- end
- end
- end
+ before do
+ expect(Gitlab::Git::Env).to receive(:all).and_return({
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
+ })
end
- context "#execute" do
- let(:env) { { "GIT_OBJECT_DIRECTORY" => project.repository.path_to_repo } }
- let(:rev_list) { Gitlab::Git::RevList.new('oldrev', 'newrev', project: project, env: env) }
-
- it "calls out to `popen` without environment variables if the record is invalid" do
- allow(rev_list).to receive(:valid?).and_return(false)
-
- expect(Open3).to receive(:popen3).with(hash_excluding(env), any_args)
-
- rev_list.execute
+ context "#new_refs" do
+ let(:rev_list) { Gitlab::Git::RevList.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
+
+ it 'calls out to `popen`' do
+ expect(Gitlab::Popen).to receive(:popen).with([
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{project.repository.path_to_repo}",
+ 'rev-list',
+ 'newrev',
+ '--not',
+ '--all'
+ ],
+ nil,
+ {
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ }).and_return(["sha1\nsha2", 0])
+
+ expect(rev_list.new_refs).to eq(%w[sha1 sha2])
end
+ end
- it "calls out to `popen` with environment variables if the record is valid" do
- allow(rev_list).to receive(:valid?).and_return(true)
-
- expect(Open3).to receive(:popen3).with(hash_including(env), any_args)
-
- rev_list.execute
+ context "#missed_ref" do
+ let(:rev_list) { Gitlab::Git::RevList.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
+
+ it 'calls out to `popen`' do
+ expect(Gitlab::Popen).to receive(:popen).with([
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{project.repository.path_to_repo}",
+ 'rev-list',
+ '--max-count=1',
+ 'oldrev',
+ '^newrev'
+ ],
+ nil,
+ {
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ }).and_return(["sha1\nsha2", 0])
+
+ expect(rev_list.missed_ref).to eq(%w[sha1 sha2])
end
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 703b41f95ac..d8b72615fab 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -211,7 +211,7 @@ describe Gitlab::GitAccess, lib: true do
target_branch = project.repository.lookup('feature')
source_branch = project.repository.create_file(
user,
- 'John Doe',
+ 'filename',
'This is the file content',
message: 'This is a good commit message',
branch_name: unprotected_branch)
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index ba45e2d758c..127cd8c78d8 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -32,12 +32,6 @@ describe Gitlab::Regex, lib: true do
it { is_expected.to match('foo@bar') }
end
- describe '.file_path_regex' do
- subject { described_class.file_path_regex }
-
- it { is_expected.to match('foo@/bar') }
- end
-
describe '.environment_slug_regex' do
subject { described_class.environment_slug_regex }
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
new file mode 100644
index 00000000000..7f21288cf88
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe Gitlab::UsageData do
+ let!(:project) { create(:empty_project) }
+ let!(:project2) { create(:empty_project) }
+ let!(:board) { create(:board, project: project) }
+
+ describe '#data' do
+ subject { Gitlab::UsageData.data }
+
+ it "gathers usage data" do
+ expect(subject.keys).to match_array(%i(
+ active_user_count
+ counts
+ recorded_at
+ mattermost_enabled
+ edition
+ version
+ uuid
+ ))
+ end
+
+ it "gathers usage counts" do
+ count_data = subject[:counts]
+
+ expect(count_data[:boards]).to eq(1)
+ expect(count_data[:projects]).to eq(2)
+
+ expect(count_data.keys).to match_array(%i(
+ boards
+ ci_builds
+ ci_pipelines
+ ci_runners
+ ci_triggers
+ deploy_keys
+ deployments
+ environments
+ groups
+ issues
+ keys
+ labels
+ lfs_objects
+ merge_requests
+ milestones
+ notes
+ projects
+ projects_prometheus_active
+ pages_domains
+ protected_branches
+ releases
+ services
+ snippets
+ todos
+ uploads
+ web_hooks
+ ))
+ end
+ end
+
+ describe '#license_usage_data' do
+ subject { Gitlab::UsageData.license_usage_data }
+
+ it "gathers license data" do
+ expect(subject[:uuid]).to eq(current_application_settings.uuid)
+ expect(subject[:version]).to eq(Gitlab::VERSION)
+ expect(subject[:active_user_count]).to eq(User.active.count)
+ expect(subject[:recorded_at]).to be_a(Time)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/user_activities_spec.rb b/spec/lib/gitlab/user_activities_spec.rb
new file mode 100644
index 00000000000..187d88c8c58
--- /dev/null
+++ b/spec/lib/gitlab/user_activities_spec.rb
@@ -0,0 +1,127 @@
+require 'spec_helper'
+
+describe Gitlab::UserActivities, :redis, lib: true do
+ let(:now) { Time.now }
+
+ describe '.record' do
+ context 'with no time given' do
+ it 'uses Time.now and records an activity in Redis' do
+ Timecop.freeze do
+ now # eager-load now
+ described_class.record(42)
+ end
+
+ Gitlab::Redis.with do |redis|
+ expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]])
+ end
+ end
+ end
+
+ context 'with a time given' do
+ it 'uses the given time and records an activity in Redis' do
+ described_class.record(42, now)
+
+ Gitlab::Redis.with do |redis|
+ expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]])
+ end
+ end
+ end
+ end
+
+ describe '.delete' do
+ context 'with a single key' do
+ context 'and key exists' do
+ it 'removes the pair from Redis' do
+ described_class.record(42, now)
+
+ Gitlab::Redis.with do |redis|
+ expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]])
+ end
+
+ subject.delete(42)
+
+ Gitlab::Redis.with do |redis|
+ expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []])
+ end
+ end
+ end
+
+ context 'and key does not exist' do
+ it 'removes the pair from Redis' do
+ Gitlab::Redis.with do |redis|
+ expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []])
+ end
+
+ subject.delete(42)
+
+ Gitlab::Redis.with do |redis|
+ expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []])
+ end
+ end
+ end
+ end
+
+ context 'with multiple keys' do
+ context 'and all keys exist' do
+ it 'removes the pair from Redis' do
+ described_class.record(41, now)
+ described_class.record(42, now)
+
+ Gitlab::Redis.with do |redis|
+ expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['41', now.to_i.to_s], ['42', now.to_i.to_s]]])
+ end
+
+ subject.delete(41, 42)
+
+ Gitlab::Redis.with do |redis|
+ expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []])
+ end
+ end
+ end
+
+ context 'and some keys does not exist' do
+ it 'removes the existing pair from Redis' do
+ described_class.record(42, now)
+
+ Gitlab::Redis.with do |redis|
+ expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]])
+ end
+
+ subject.delete(41, 42)
+
+ Gitlab::Redis.with do |redis|
+ expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []])
+ end
+ end
+ end
+ end
+ end
+
+ describe 'Enumerable' do
+ before do
+ described_class.record(40, now)
+ described_class.record(41, now)
+ described_class.record(42, now)
+ end
+
+ it 'allows to read the activities sequentially' do
+ expected = { '40' => now.to_i.to_s, '41' => now.to_i.to_s, '42' => now.to_i.to_s }
+
+ actual = described_class.new.each_with_object({}) do |(key, time), actual|
+ actual[key] = time
+ end
+
+ expect(actual).to eq(expected)
+ end
+
+ context 'with many records' do
+ before do
+ 1_000.times { |i| described_class.record(i, now) }
+ end
+
+ it 'is possible to loop through all the records' do
+ expect(described_class.new.count).to eq(1_000)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb
new file mode 100644
index 00000000000..1db9bc002ae
--- /dev/null
+++ b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb
@@ -0,0 +1,49 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170324160416_migrate_user_activities_to_users_last_activity_on.rb')
+
+describe MigrateUserActivitiesToUsersLastActivityOn, :redis do
+ let(:migration) { described_class.new }
+ let!(:user_active_1) { create(:user) }
+ let!(:user_active_2) { create(:user) }
+
+ def record_activity(user, time)
+ Gitlab::Redis.with do |redis|
+ redis.zadd(described_class::USER_ACTIVITY_SET_KEY, time.to_i, user.username)
+ end
+ end
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ record_activity(user_active_1, described_class::TIME_WHEN_ACTIVITY_SET_WAS_INTRODUCED + 2.months)
+ record_activity(user_active_2, described_class::TIME_WHEN_ACTIVITY_SET_WAS_INTRODUCED + 3.months)
+ mute_stdout { migration.up }
+ end
+
+ describe '#up' do
+ it 'fills last_activity_on from the legacy Redis Sorted Set' do
+ expect(user_active_1.reload.last_activity_on).to eq((described_class::TIME_WHEN_ACTIVITY_SET_WAS_INTRODUCED + 2.months).to_date)
+ expect(user_active_2.reload.last_activity_on).to eq((described_class::TIME_WHEN_ACTIVITY_SET_WAS_INTRODUCED + 3.months).to_date)
+ end
+ end
+
+ describe '#down' do
+ it 'sets last_activity_on to NULL for all users' do
+ mute_stdout { migration.down }
+
+ expect(user_active_1.reload.last_activity_on).to be_nil
+ expect(user_active_2.reload.last_activity_on).to be_nil
+ end
+ end
+
+ def mute_stdout
+ orig_stdout = $stdout
+ $stdout = StringIO.new
+ yield
+ $stdout = orig_stdout
+ end
+end
diff --git a/spec/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb
new file mode 100644
index 00000000000..dacaa834aa9
--- /dev/null
+++ b/spec/migrations/migrate_user_project_view_spec.rb
@@ -0,0 +1,17 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_project_view.rb')
+
+describe MigrateUserProjectView do
+ let(:migration) { described_class.new }
+ let!(:user) { create(:user, project_view: 'readme') }
+
+ describe '#up' do
+ it 'updates project view setting with new value' do
+ migration.up
+
+ expect(user.reload.project_view).to eq('files')
+ end
+ end
+end
diff --git a/spec/migrations/schema_spec.rb b/spec/migrations/schema_spec.rb
new file mode 100644
index 00000000000..e132529d8d8
--- /dev/null
+++ b/spec/migrations/schema_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+# Check consistency of db/schema.rb version, migrations' timestamps, and the latest migration timestamp
+# stored in the database's schema_migrations table.
+
+describe ActiveRecord::Schema do
+ let(:latest_migration_timestamp) do
+ migrations = Dir[Rails.root.join('db', 'migrate', '*'), Rails.root.join('db', 'post_migrate', '*')]
+ migrations.map { |migration| File.basename(migration).split('_').first.to_i }.max
+ end
+
+ it '> schema version equals last migration timestamp' do
+ defined_schema_version = File.open(Rails.root.join('db', 'schema.rb')) do |file|
+ file.find { |line| line =~ /ActiveRecord::Schema.define/ }
+ end.match(/(\d+)/)[0].to_i
+
+ expect(defined_schema_version).to eq(latest_migration_timestamp)
+ end
+
+ it '> schema version should equal the latest migration timestamp stored in schema_migrations table' do
+ expect(latest_migration_timestamp).to eq(ActiveRecord::Migrator.current_version.to_i)
+ end
+end
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 4e71597521d..ced93c8f762 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -29,7 +29,8 @@ RSpec.describe AbuseReport, type: :model do
it 'lets a worker delete the user' do
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id,
- delete_solo_owned_groups: true)
+ delete_solo_owned_groups: true,
+ hard_delete: true)
subject.remove_user(deleted_by: user)
end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 0f29766db41..e5dd57fc4bb 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -55,13 +55,13 @@ describe Blob do
describe '#pdf?' do
it 'is falsey when file extension is not .pdf' do
- git_blob = double(name: 'git_blob.txt')
+ git_blob = Gitlab::Git::Blob.new(name: 'git_blob.txt')
expect(described_class.decorate(git_blob)).not_to be_pdf
end
it 'is truthy when file extension is .pdf' do
- git_blob = double(name: 'git_blob.pdf')
+ git_blob = Gitlab::Git::Blob.new(name: 'git_blob.pdf')
expect(described_class.decorate(git_blob)).to be_pdf
end
@@ -140,7 +140,7 @@ describe Blob do
stl?: false
)
- described_class.decorate(double).tap do |blob|
+ described_class.decorate(Gitlab::Git::Blob.new({})).tap do |blob|
allow(blob).to receive_messages(overrides)
end
end
@@ -158,7 +158,7 @@ describe Blob do
it 'handles SVGs' do
blob = stubbed_blob(text?: true, svg?: true)
- expect(blob.to_partial_path(project)).to eq 'image'
+ expect(blob.to_partial_path(project)).to eq 'svg'
end
it 'handles images' do
@@ -167,7 +167,7 @@ describe Blob do
end
it 'handles text' do
- blob = stubbed_blob(text?: true)
+ blob = stubbed_blob(text?: true, name: 'test.txt')
expect(blob.to_partial_path(project)).to eq 'text'
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 980a1b70ef5..ce31c8ed94c 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -389,31 +389,32 @@ eos
end
end
- describe '#raw_diffs' do
- context 'Gitaly commit_raw_diffs feature enabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
- end
-
- context 'when a truthy deltas_only is not passed to args' do
- it 'fetches diffs from Gitaly server' do
- expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
- with(commit)
-
- commit.raw_diffs
- end
- end
-
- context 'when a truthy deltas_only is passed to args' do
- it 'fetches diffs using Rugged' do
- opts = { deltas_only: true }
-
- expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
- expect(commit.raw).to receive(:diffs).with(opts)
-
- commit.raw_diffs(opts)
- end
- end
- end
- end
+ # describe '#raw_diffs' do
+ # TODO: Uncomment when feature is reenabled
+ # context 'Gitaly commit_raw_diffs feature enabled' do
+ # before do
+ # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
+ # end
+ #
+ # context 'when a truthy deltas_only is not passed to args' do
+ # it 'fetches diffs from Gitaly server' do
+ # expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
+ # with(commit)
+ #
+ # commit.raw_diffs
+ # end
+ # end
+ #
+ # context 'when a truthy deltas_only is passed to args' do
+ # it 'fetches diffs using Rugged' do
+ # opts = { deltas_only: true }
+ #
+ # expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
+ # expect(commit.raw).to receive(:diffs).with(opts)
+ #
+ # commit.raw_diffs(opts)
+ # end
+ # end
+ # end
+ # end
end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 6151d53cd91..de0069bdcac 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -1,9 +1,6 @@
require 'spec_helper'
describe CacheMarkdownField do
- caching_classes = CacheMarkdownField::CACHING_CLASSES
- CacheMarkdownField::CACHING_CLASSES = ["ThingWithMarkdownFields"].freeze
-
# The minimum necessary ActiveModel to test this concern
class ThingWithMarkdownFields
include ActiveModel::Model
@@ -27,18 +24,19 @@ describe CacheMarkdownField do
cache_markdown_field :foo
cache_markdown_field :baz, pipeline: :single_line
- def self.add_attr(attr_name)
- self.attribute_names += [attr_name]
- define_attribute_methods(attr_name)
- attr_reader(attr_name)
- define_method("#{attr_name}=") do |val|
- send("#{attr_name}_will_change!") unless val == send(attr_name)
- instance_variable_set("@#{attr_name}", val)
+ def self.add_attr(name)
+ self.attribute_names += [name]
+ define_attribute_methods(name)
+ attr_reader(name)
+ define_method("#{name}=") do |value|
+ write_attribute(name, value)
end
end
- [:foo, :foo_html, :bar, :baz, :baz_html].each do |attr_name|
- add_attr(attr_name)
+ add_attr :cached_markdown_version
+
+ [:foo, :foo_html, :bar, :baz, :baz_html].each do |name|
+ add_attr(name)
end
def initialize(*)
@@ -48,6 +46,15 @@ describe CacheMarkdownField do
clear_changes_information
end
+ def read_attribute(name)
+ instance_variable_get("@#{name}")
+ end
+
+ def write_attribute(name, value)
+ send("#{name}_will_change!") unless value == read_attribute(name)
+ instance_variable_set("@#{name}", value)
+ end
+
def save
run_callbacks :save do
changes_applied
@@ -55,127 +62,236 @@ describe CacheMarkdownField do
end
end
- CacheMarkdownField::CACHING_CLASSES = caching_classes
-
def thing_subclass(new_attr)
Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
end
- let(:markdown) { "`Foo`" }
- let(:html) { "<p><code>Foo</code></p>" }
+ let(:markdown) { '`Foo`' }
+ let(:html) { '<p dir="auto"><code>Foo</code></p>' }
- let(:updated_markdown) { "`Bar`" }
- let(:updated_html) { "<p dir=\"auto\"><code>Bar</code></p>" }
+ let(:updated_markdown) { '`Bar`' }
+ let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
- subject { ThingWithMarkdownFields.new(foo: markdown, foo_html: html) }
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_VERSION) }
- describe ".attributes" do
- it "excludes cache attributes" do
- expect(thing_subclass(:qux).new.attributes.keys.sort).to eq(%w[bar baz foo qux])
+ describe '.attributes' do
+ it 'excludes cache attributes' do
+ expect(thing.attributes.keys.sort).to eq(%w[bar baz foo])
end
end
- describe ".cache_markdown_field" do
- it "refuses to allow untracked classes" do
- expect { thing_subclass(:qux).__send__(:cache_markdown_field, :qux) }.to raise_error(RuntimeError)
+ context 'an unchanged markdown field' do
+ before do
+ thing.foo = thing.foo
+ thing.save
end
+
+ it { expect(thing.foo).to eq(markdown) }
+ it { expect(thing.foo_html).to eq(html) }
+ it { expect(thing.foo_html_changed?).not_to be_truthy }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
end
- context "an unchanged markdown field" do
+ context 'a changed markdown field' do
before do
- subject.foo = subject.foo
- subject.save
+ thing.foo = updated_markdown
+ thing.save
end
- it { expect(subject.foo).to eq(markdown) }
- it { expect(subject.foo_html).to eq(html) }
- it { expect(subject.foo_html_changed?).not_to be_truthy }
+ it { expect(thing.foo_html).to eq(updated_html) }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
end
- context "a changed markdown field" do
+ context 'a non-markdown field changed' do
+ before do
+ thing.bar = 'OK'
+ thing.save
+ end
+
+ it { expect(thing.bar).to eq('OK') }
+ it { expect(thing.foo).to eq(markdown) }
+ it { expect(thing.foo_html).to eq(html) }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
+ end
+
+ context 'version is out of date' do
+ let(:thing) { ThingWithMarkdownFields.new(foo: updated_markdown, foo_html: html, cached_markdown_version: nil) }
+
before do
- subject.foo = updated_markdown
- subject.save
+ thing.save
+ end
+
+ it { expect(thing.foo_html).to eq(updated_html) }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
+ end
+
+ describe '#cached_html_up_to_date?' do
+ subject { thing.cached_html_up_to_date?(:foo) }
+
+ it 'returns false when the version is absent' do
+ thing.cached_markdown_version = nil
+
+ is_expected.to be_falsy
+ end
+
+ it 'returns false when the version is too early' do
+ thing.cached_markdown_version -= 1
+
+ is_expected.to be_falsy
+ end
+
+ it 'returns false when the version is too late' do
+ thing.cached_markdown_version += 1
+
+ is_expected.to be_falsy
+ end
+
+ it 'returns true when the version is just right' do
+ thing.cached_markdown_version = CacheMarkdownField::CACHE_VERSION
+
+ is_expected.to be_truthy
end
- it { expect(subject.foo_html).to eq(updated_html) }
+ it 'returns false if markdown has been changed but html has not' do
+ thing.foo = updated_html
+
+ is_expected.to be_falsy
+ end
+
+ it 'returns true if markdown has not been changed but html has' do
+ thing.foo_html = updated_html
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns true if markdown and html have both been changed' do
+ thing.foo = updated_markdown
+ thing.foo_html = updated_html
+
+ is_expected.to be_truthy
+ end
end
- context "a non-markdown field changed" do
+ describe '#refresh_markdown_cache!' do
before do
- subject.bar = "OK"
- subject.save
+ thing.foo = updated_markdown
+ end
+
+ context 'do_update: false' do
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache!
+
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.foo_html_changed?).to be_truthy
+ expect(thing.baz_html_changed?).to be_truthy
+ end
+
+ it 'does not save the result' do
+ expect(thing).not_to receive(:update_columns)
+
+ thing.refresh_markdown_cache!
+ end
+
+ it 'updates the markdown cache version' do
+ thing.cached_markdown_version = nil
+ thing.refresh_markdown_cache!
+
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
+ end
end
- it { expect(subject.bar).to eq("OK") }
- it { expect(subject.foo).to eq(markdown) }
- it { expect(subject.foo_html).to eq(html) }
+ context 'do_update: true' do
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache!(do_update: true)
+
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.foo_html_changed?).to be_truthy
+ expect(thing.baz_html_changed?).to be_truthy
+ end
+
+ it 'skips saving if not persisted' do
+ expect(thing).to receive(:persisted?).and_return(false)
+ expect(thing).not_to receive(:update_columns)
+
+ thing.refresh_markdown_cache!(do_update: true)
+ end
+
+ it 'saves the changes using #update_columns' do
+ expect(thing).to receive(:persisted?).and_return(true)
+ expect(thing).to receive(:update_columns)
+ .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
+
+ thing.refresh_markdown_cache!(do_update: true)
+ end
+ end
end
describe '#banzai_render_context' do
- it "sets project to nil if the object lacks a project" do
- context = subject.banzai_render_context(:foo)
- expect(context).to have_key(:project)
+ subject(:context) { thing.banzai_render_context(:foo) }
+
+ it 'sets project to nil if the object lacks a project' do
+ is_expected.to have_key(:project)
expect(context[:project]).to be_nil
end
- it "excludes author if the object lacks an author" do
- context = subject.banzai_render_context(:foo)
- expect(context).not_to have_key(:author)
+ it 'excludes author if the object lacks an author' do
+ is_expected.not_to have_key(:author)
end
- it "raises if the context for an unrecognised field is requested" do
- expect{subject.banzai_render_context(:not_found)}.to raise_error(ArgumentError)
+ it 'raises if the context for an unrecognised field is requested' do
+ expect { thing.banzai_render_context(:not_found) }.to raise_error(ArgumentError)
end
- it "includes the pipeline" do
- context = subject.banzai_render_context(:baz)
- expect(context[:pipeline]).to eq(:single_line)
+ it 'includes the pipeline' do
+ baz = thing.banzai_render_context(:baz)
+
+ expect(baz[:pipeline]).to eq(:single_line)
end
- it "returns copies of the context template" do
- template = subject.cached_markdown_fields[:baz]
- copy = subject.banzai_render_context(:baz)
+ it 'returns copies of the context template' do
+ template = thing.cached_markdown_fields[:baz]
+ copy = thing.banzai_render_context(:baz)
+
expect(copy).not_to be(template)
end
- context "with a project" do
- subject { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project) }
+ context 'with a project' do
+ let(:thing) { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project_value) }
- it "sets the project in the context" do
- context = subject.banzai_render_context(:foo)
- expect(context).to have_key(:project)
- expect(context[:project]).to eq(:project)
+ it 'sets the project in the context' do
+ is_expected.to have_key(:project)
+ expect(context[:project]).to eq(:project_value)
end
- it "invalidates the cache when project changes" do
- subject.project = :new_project
+ it 'invalidates the cache when project changes' do
+ thing.project = :new_project
allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
- subject.save
+ thing.save
- expect(subject.foo_html).to eq(updated_html)
- expect(subject.baz_html).to eq(updated_html)
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.baz_html).to eq(updated_html)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
end
end
- context "with an author" do
- subject { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author) }
+ context 'with an author' do
+ let(:thing) { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author_value) }
- it "sets the author in the context" do
- context = subject.banzai_render_context(:foo)
- expect(context).to have_key(:author)
- expect(context[:author]).to eq(:author)
+ it 'sets the author in the context' do
+ is_expected.to have_key(:author)
+ expect(context[:author]).to eq(:author_value)
end
- it "invalidates the cache when author changes" do
- subject.author = :new_author
+ it 'invalidates the cache when author changes' do
+ thing.author = :new_author
allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
- subject.save
+ thing.save
- expect(subject.foo_html).to eq(updated_html)
- expect(subject.baz_html).to eq(updated_html)
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.baz_html).to eq(updated_html)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
end
end
end
diff --git a/spec/models/concerns/ignorable_column_spec.rb b/spec/models/concerns/ignorable_column_spec.rb
new file mode 100644
index 00000000000..dba9fe43327
--- /dev/null
+++ b/spec/models/concerns/ignorable_column_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe IgnorableColumn do
+ let :base_class do
+ Class.new do
+ def self.columns
+ # This method does not have access to "double"
+ [Struct.new(:name).new('id'), Struct.new(:name).new('title')]
+ end
+ end
+ end
+
+ let :model do
+ Class.new(base_class) do
+ include IgnorableColumn
+ end
+ end
+
+ describe '.columns' do
+ it 'returns the columns, excluding the ignored ones' do
+ model.ignore_column(:title)
+
+ expect(model.columns.map(&:name)).to eq(%w(id))
+ end
+ end
+
+ describe '.ignored_columns' do
+ it 'returns a Set' do
+ expect(model.ignored_columns).to be_an_instance_of(Set)
+ end
+
+ it 'returns the names of the ignored columns' do
+ model.ignore_column(:title)
+
+ expect(model.ignored_columns).to eq(Set.new(%w(title)))
+ end
+ end
+end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index f7ee0b57072..eff41d85972 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -4,7 +4,7 @@ describe ContainerRepository do
let(:group) { create(:group, name: 'group') }
let(:project) { create(:project, path: 'test', group: group) }
- let(:container_repository) do
+ let(:repository) do
create(:container_repository, name: 'my_image', project: project)
end
@@ -23,48 +23,58 @@ describe ContainerRepository do
describe 'associations' do
it 'belongs to the project' do
- expect(container_repository).to belong_to(:project)
+ expect(repository).to belong_to(:project)
end
end
describe '#tag' do
it 'has a test tag' do
- expect(container_repository.tag('test')).not_to be_nil
+ expect(repository.tag('test')).not_to be_nil
end
end
describe '#path' do
- it 'returns a full path to the repository' do
- expect(container_repository.path).to eq('group/test/my_image')
+ context 'when project path does not contain uppercase letters' do
+ it 'returns a full path to the repository' do
+ expect(repository.path).to eq('group/test/my_image')
+ end
+ end
+
+ context 'when path contains uppercase letters' do
+ let(:project) { create(:project, path: 'MY_PROJECT', group: group) }
+
+ it 'returns a full path without capital letters' do
+ expect(repository.path).to eq('group/my_project/my_image')
+ end
end
end
describe '#manifest' do
- subject { container_repository.manifest }
-
- it { is_expected.not_to be_nil }
+ it 'returns non-empty manifest' do
+ expect(repository.manifest).not_to be_nil
+ end
end
describe '#valid?' do
- subject { container_repository.valid? }
-
- it { is_expected.to be_truthy }
+ it 'is a valid repository' do
+ expect(repository).to be_valid
+ end
end
describe '#tags' do
- subject { container_repository.tags }
-
- it { is_expected.not_to be_empty }
+ it 'returns non-empty tags list' do
+ expect(repository.tags).not_to be_empty
+ end
end
describe '#has_tags?' do
it 'has tags' do
- expect(container_repository).to have_tags
+ expect(repository).to have_tags
end
end
describe '#delete_tags!' do
- let(:container_repository) do
+ let(:repository) do
create(:container_repository, name: 'my_image',
tags: %w[latest rc1],
project: project)
@@ -72,21 +82,36 @@ describe ContainerRepository do
context 'when action succeeds' do
it 'returns status that indicates success' do
- expect(container_repository.client)
+ expect(repository.client)
.to receive(:delete_repository_tag)
.and_return(true)
- expect(container_repository.delete_tags!).to be_truthy
+ expect(repository.delete_tags!).to be_truthy
end
end
context 'when action fails' do
it 'returns status that indicates failure' do
- expect(container_repository.client)
+ expect(repository.client)
.to receive(:delete_repository_tag)
.and_return(false)
- expect(container_repository.delete_tags!).to be_falsey
+ expect(repository.delete_tags!).to be_falsey
+ end
+ end
+ end
+
+ describe '#location' do
+ context 'when registry is running on a custom port' do
+ before do
+ stub_container_registry_config(enabled: true,
+ api_url: 'http://registry.gitlab:5000',
+ host_port: 'registry.gitlab:5000')
+ end
+
+ it 'returns a full location of the repository' do
+ expect(repository.location)
+ .to eq 'registry.gitlab:5000/group/test/my_image'
end
end
end
@@ -102,7 +127,7 @@ describe ContainerRepository do
context 'when repository is not a root repository' do
it 'returns false' do
- expect(container_repository).not_to be_root_repository
+ expect(repository).not_to be_root_repository
end
end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index fb80b74b226..f32b6b99b3d 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -155,6 +155,23 @@ describe DiffNote, models: true do
end
end
+ describe '#latest_merge_request_diff' do
+ context 'when active' do
+ it 'returns the current merge request diff' do
+ expect(subject.latest_merge_request_diff).to eq(merge_request.merge_request_diff)
+ end
+ end
+
+ context 'when outdated' do
+ let!(:old_merge_request_diff) { merge_request.merge_request_diff }
+ let!(:new_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: commit.diff_refs) }
+
+ it 'returns the latest merge request diff that this diff note applied to' do
+ expect(subject.latest_merge_request_diff).to eq(old_merge_request_diff)
+ end
+ end
+ end
+
describe "creation" do
describe "updating of position" do
context "when noteable is a commit" do
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index af7753caba6..070716e859a 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -110,17 +110,18 @@ describe Environment, models: true do
end
end
- context 'Gitaly find_ref_name feature enabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:find_ref_name).and_return(true)
- end
-
- it 'calls GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:find_ref_name)
-
- environment.first_deployment_for(commit)
- end
- end
+ # TODO: Uncomment when feature is reenabled
+ # context 'Gitaly find_ref_name feature enabled' do
+ # before do
+ # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:find_ref_name).and_return(true)
+ # end
+ #
+ # it 'calls GitalyClient' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:find_ref_name)
+ #
+ # environment.first_deployment_for(commit)
+ # end
+ # end
end
describe '#environment_type' do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 4bdd46a581d..11befd4edfe 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -134,15 +134,6 @@ describe Issue, models: true do
end
end
- describe '#is_being_reassigned?' do
- it 'returns issues assigned to user' do
- user = create(:user)
- create_list(:issue, 2, assignee: user)
-
- expect(Issue.open_for(user).count).to eq 2
- end
- end
-
describe '#closed_by_merge_requests' do
let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project)}
@@ -370,7 +361,10 @@ describe Issue, models: true do
it 'updates when assignees change' do
user1 = create(:user)
user2 = create(:user)
- issue = create(:issue, assignee: user1)
+ project = create(:empty_project)
+ issue = create(:issue, assignee: user1, project: project)
+ project.add_developer(user1)
+ project.add_developer(user2)
expect(user1.assigned_open_issues_count).to eq(1)
expect(user2.assigned_open_issues_count).to eq(0)
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index a9139f7d4ab..80ca19acdda 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -42,11 +42,27 @@ describe Label, models: true do
end
end
+ describe '#color' do
+ it 'strips color' do
+ label = described_class.new(color: ' #abcdef ')
+ label.valid?
+
+ expect(label.color).to eq('#abcdef')
+ end
+ end
+
describe '#title' do
it 'sanitizes title' do
label = described_class.new(title: '<b>foo & bar?</b>')
expect(label.title).to eq('foo & bar?')
end
+
+ it 'strips title' do
+ label = described_class.new(title: ' label ')
+ label.valid?
+
+ expect(label.title).to eq('label')
+ end
end
describe 'priorization' do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index c720cc9f2c2..b0f3657d3b5 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -386,6 +386,31 @@ describe Member, models: true do
end
end
+ describe '.add_users' do
+ %w[project group].each do |source_type|
+ context "when source is a #{source_type}" do
+ let!(:source) { create(source_type, :public, :access_requestable) }
+ let!(:user) { create(:user) }
+ let!(:admin) { create(:admin) }
+
+ it 'returns a <Source>Member objects' do
+ members = described_class.add_users(source, [user], :master)
+
+ expect(members).to be_a Array
+ expect(members.first).to be_a "#{source_type.classify}Member".constantize
+ expect(members.first).to be_persisted
+ end
+
+ it 'returns an empty array' do
+ members = described_class.add_users(source, [], :master)
+
+ expect(members).to be_a Array
+ expect(members).to be_empty
+ end
+ end
+ end
+ end
+
describe '#accept_request' do
let(:member) { create(:project_member, requested_at: Time.now.utc) }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 024380b7ebb..17765b25856 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -13,12 +13,12 @@ describe GroupMember, models: true do
end
end
- describe '.add_users_to_group' do
+ describe '.add_users' do
it 'adds the given users to the given group' do
group = create(:group)
users = create_list(:user, 2)
- described_class.add_users_to_group(
+ described_class.add_users(
group,
[users.first.id, users.second],
described_class::MASTER
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 90b3a2ba42d..415d3e7b200 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -820,15 +820,17 @@ describe MergeRequest, models: true do
user1 = create(:user)
user2 = create(:user)
mr = create(:merge_request, assignee: user1)
+ mr.project.add_developer(user1)
+ mr.project.add_developer(user2)
- expect(user1.assigned_open_merge_request_count).to eq(1)
- expect(user2.assigned_open_merge_request_count).to eq(0)
+ expect(user1.assigned_open_merge_requests_count).to eq(1)
+ expect(user2.assigned_open_merge_requests_count).to eq(0)
mr.assignee = user2
mr.save
- expect(user1.assigned_open_merge_request_count).to eq(0)
- expect(user2.assigned_open_merge_request_count).to eq(1)
+ expect(user1.assigned_open_merge_requests_count).to eq(0)
+ expect(user2.assigned_open_merge_requests_count).to eq(1)
end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 3c4bf3f4ddb..557ea97b008 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -622,12 +622,22 @@ describe Note, models: true do
describe 'expiring ETag cache' do
let(:note) { build(:note_on_issue) }
- it "expires cache for note's issue when note is saved" do
+ def expect_expiration(note)
expect_any_instance_of(Gitlab::EtagCaching::Store)
.to receive(:touch)
.with("/#{note.project.namespace.to_param}/#{note.project.to_param}/noteable/issue/#{note.noteable.id}/notes")
+ end
+
+ it "expires cache for note's issue when note is saved" do
+ expect_expiration(note)
note.save!
end
+
+ it "expires cache for note's issue when note is destroyed" do
+ expect_expiration(note)
+
+ note.destroy!
+ end
end
end
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
index c98e7ee14fd..592c90cda36 100644
--- a/spec/models/project_services/chat_notification_service_spec.rb
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -1,11 +1,29 @@
require 'spec_helper'
describe ChatNotificationService, models: true do
- describe "Associations" do
+ describe 'Associations' do
before do
allow(subject).to receive(:activated?).and_return(true)
end
it { is_expected.to validate_presence_of :webhook }
end
+
+ describe '#can_test?' do
+ context 'with empty repository' do
+ it 'returns false' do
+ subject.project = create(:empty_project, :empty_repo)
+
+ expect(subject.can_test?).to be false
+ end
+ end
+
+ context 'with repository' do
+ it 'returns true' do
+ subject.project = create(:project)
+
+ expect(subject.can_test?).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index d805e65b3c6..74d5ebc6db0 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -171,6 +171,27 @@ describe Repository, models: true do
end
end
+ describe '#commits' do
+ it 'sets follow when path is a single path' do
+ expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice
+
+ repository.commits('master', path: 'README.md')
+ repository.commits('master', path: ['README.md'])
+ end
+
+ it 'does not set follow when path is multiple paths' do
+ expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
+
+ repository.commits('master', path: ['README.md', 'CHANGELOG'])
+ end
+
+ it 'does not set follow when there are no paths' do
+ expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
+
+ repository.commits('master')
+ end
+ end
+
describe '#find_commits_by_message' do
it 'returns commits with messages containing a given string' do
commit_ids = repository.find_commits_by_message('submodule').map(&:id)
@@ -1259,7 +1280,6 @@ describe Repository, models: true do
:changelog,
:license,
:contributing,
- :version,
:gitignore,
:koding,
:gitlab_ci,
@@ -1283,8 +1303,6 @@ describe Repository, models: true do
describe '#after_import' do
it 'flushes and builds the cache' do
expect(repository).to receive(:expire_content_cache)
- expect(repository).to receive(:expire_tags_cache)
- expect(repository).to receive(:expire_branches_cache)
repository.after_import
end
@@ -1831,16 +1849,17 @@ describe Repository, models: true do
end
end
- describe '#is_ancestor?' do
- context 'Gitaly is_ancestor feature enabled' do
- it 'asks Gitaly server if it\'s an ancestor' do
- commit = repository.commit
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
- expect(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).
- with(repository.raw_repository, commit.id, commit.id).and_return(true)
-
- expect(repository.is_ancestor?(commit.id, commit.id)).to be true
- end
- end
- end
+ # TODO: Uncomment when feature is reenabled
+ # describe '#is_ancestor?' do
+ # context 'Gitaly is_ancestor feature enabled' do
+ # it 'asks Gitaly server if it\'s an ancestor' do
+ # commit = repository.commit
+ # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
+ # expect(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).
+ # with(repository.raw_repository, commit.id, commit.id).and_return(true)
+ #
+ # expect(repository.is_ancestor?(commit.id, commit.id)).to be true
+ # end
+ # end
+ # end
end
diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb
index c4ec7625cb0..838fba6c92d 100644
--- a/spec/models/spam_log_spec.rb
+++ b/spec/models/spam_log_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe SpamLog, models: true do
+ let(:admin) { create(:admin) }
+
describe 'associations' do
it { is_expected.to belong_to(:user) }
end
@@ -13,13 +15,18 @@ describe SpamLog, models: true do
it 'blocks the user' do
spam_log = build(:spam_log)
- expect { spam_log.remove_user }.to change { spam_log.user.blocked? }.to(true)
+ expect { spam_log.remove_user(deleted_by: admin) }.to change { spam_log.user.blocked? }.to(true)
end
it 'removes the user' do
spam_log = build(:spam_log)
+ user = spam_log.user
+
+ Sidekiq::Testing.inline! do
+ spam_log.remove_user(deleted_by: admin)
+ end
- expect { spam_log.remove_user }.to change { User.count }.by(-1)
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 9de16c41e94..0a2860f2505 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -24,9 +24,7 @@ describe User, models: true do
it { is_expected.to have_many(:recent_events).class_name('Event') }
it { is_expected.to have_many(:issues).dependent(:restrict_with_exception) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
- it { is_expected.to have_many(:assigned_issues).dependent(:nullify) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
- it { is_expected.to have_many(:assigned_merge_requests).dependent(:nullify) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
@@ -1631,4 +1629,16 @@ describe User, models: true do
end
end
end
+
+ context '.active' do
+ before do
+ User.ghost
+ create(:user, name: 'user', state: 'active')
+ create(:user, name: 'user', state: 'blocked')
+ end
+
+ it 'only counts active and non internal users' do
+ expect(User.active.count).to eq(1)
+ end
+ end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 5c34ff04152..2077c14ff7a 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -22,7 +22,8 @@ describe GroupPolicy, models: true do
:admin_group,
:admin_namespace,
:admin_group_member,
- :change_visibility_level
+ :change_visibility_level,
+ :create_subgroup
]
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index a10d876ffad..42dbab586cd 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -599,8 +599,7 @@ describe API::Commits, api: true do
post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown'
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically.
- A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.')
+ expect(json_response['message']).to include('Sorry, we cannot cherry-pick this commit automatically.')
end
it 'returns 400 if you are not allowed to push to the target branch' do
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 8012530f139..6db2faed76b 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -205,7 +205,7 @@ describe API::Files, api: true do
it "returns a 400 if editor fails to create file" do
allow_any_instance_of(Repository).to receive(:create_file).
- and_return(false)
+ and_raise(Repository::CommitError, 'Cannot create file')
post api(route("any%2Etxt"), user), valid_params
@@ -299,8 +299,8 @@ describe API::Files, api: true do
expect(response).to have_http_status(400)
end
- it "returns a 400 if fails to create file" do
- allow_any_instance_of(Repository).to receive(:delete_file).and_return(false)
+ it "returns a 400 if fails to delete file" do
+ allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file')
delete api(route(file_path), user), valid_params
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index eed45d37444..3d6010ede73 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -147,10 +147,31 @@ describe API::Internal, api: true do
end
end
- describe "POST /internal/allowed" do
+ describe "POST /internal/allowed", :redis do
context "access granted" do
before do
project.team << [user, :developer]
+ Timecop.freeze
+ end
+
+ after do
+ Timecop.return
+ end
+
+ context 'with env passed as a JSON' do
+ it 'sets env in RequestStore' do
+ expect(Gitlab::Git::Env).to receive(:set).with({
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ })
+
+ push(key, project.wiki, env: {
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
+ }.to_json)
+
+ expect(response).to have_http_status(200)
+ end
end
context "git push with project.wiki" do
@@ -160,6 +181,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
+ expect(user).not_to have_an_activity_record
end
end
@@ -170,6 +192,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
+ expect(user).to have_an_activity_record
end
end
@@ -180,6 +203,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(user).to have_an_activity_record
end
end
@@ -190,6 +214,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(user).not_to have_an_activity_record
end
context 'project as /namespace/project' do
@@ -225,6 +250,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
+ expect(user).not_to have_an_activity_record
end
end
@@ -234,6 +260,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
+ expect(user).not_to have_an_activity_record
end
end
end
@@ -251,6 +278,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
+ expect(user).not_to have_an_activity_record
end
end
@@ -260,6 +288,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
+ expect(user).not_to have_an_activity_record
end
end
end
@@ -463,7 +492,7 @@ describe API::Internal, api: true do
)
end
- def push(key, project, protocol = 'ssh')
+ def push(key, project, protocol = 'ssh', env: nil)
post(
api("/internal/allowed"),
changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master',
@@ -471,7 +500,8 @@ describe API::Internal, api: true do
project: project.repository.path_to_repo,
action: 'git-receive-pack',
secret_token: secret_token,
- protocol: protocol
+ protocol: protocol,
+ env: env
)
end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index b1f8c249092..b1603233f9e 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -22,8 +22,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
context "authorized user" do
it "returns project hooks" do
get api("/projects/#{project.id}/hooks", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response).to include_pagination_headers
expect(json_response.count).to eq(1)
@@ -43,6 +43,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
context "unauthorized user" do
it "does not access project hooks" do
get api("/projects/#{project.id}/hooks", user3)
+
expect(response).to have_http_status(403)
end
end
@@ -52,6 +53,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
context "authorized user" do
it "returns a project hook" do
get api("/projects/#{project.id}/hooks/#{hook.id}", user)
+
expect(response).to have_http_status(200)
expect(json_response['url']).to eq(hook.url)
expect(json_response['issues_events']).to eq(hook.issues_events)
@@ -67,6 +69,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
it "returns a 404 error if hook id is not available" do
get api("/projects/#{project.id}/hooks/1234", user)
+
expect(response).to have_http_status(404)
end
end
@@ -88,7 +91,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
it "adds hook to project" do
expect do
post api("/projects/#{project.id}/hooks", user),
- url: "http://example.com", issues_events: true, wiki_page_events: true
+ url: "http://example.com", issues_events: true, wiki_page_events: true,
+ job_events: true
end.to change {project.hooks.count}.by(1)
expect(response).to have_http_status(201)
@@ -98,7 +102,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false)
- expect(json_response['job_events']).to eq(false)
+ expect(json_response['job_events']).to eq(true)
expect(json_response['pipeline_events']).to eq(false)
expect(json_response['wiki_page_events']).to eq(true)
expect(json_response['enable_ssl_verification']).to eq(true)
@@ -136,7 +140,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
describe "PUT /projects/:id/hooks/:hook_id" do
it "updates an existing project hook" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user),
- url: 'http://example.org', push_events: false
+ url: 'http://example.org', push_events: false, job_events: true
+
expect(response).to have_http_status(200)
expect(json_response['url']).to eq('http://example.org')
expect(json_response['issues_events']).to eq(hook.issues_events)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 2e291eb3cea..40365585a56 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -24,6 +24,7 @@ describe API::Projects, :api do
namespace: user.namespace,
merge_requests_enabled: false,
issues_enabled: false, wiki_enabled: false,
+ builds_enabled: false,
snippets_enabled: false)
end
let(:project_member3) do
@@ -342,6 +343,7 @@ describe API::Projects, :api do
project = attributes_for(:project, {
path: 'camelCasePath',
issues_enabled: false,
+ jobs_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
only_allow_merge_if_pipeline_succeeds: false,
@@ -351,6 +353,8 @@ describe API::Projects, :api do
post api('/projects', user), project
+ expect(response).to have_http_status(201)
+
project.each_pair do |k, v|
next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
expect(json_response[k.to_s]).to eq(v)
@@ -1076,10 +1080,21 @@ describe API::Projects, :api do
before { project_member3 }
before { project_member2 }
+ it 'returns 400 when nothing sent' do
+ project_param = {}
+
+ put api("/projects/#{project.id}", user), project_param
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to match('at least one parameter must be provided')
+ end
+
context 'when unauthenticated' do
it 'returns authentication error' do
project_param = { name: 'bar' }
+
put api("/projects/#{project.id}"), project_param
+
expect(response).to have_http_status(401)
end
end
@@ -1087,8 +1102,11 @@ describe API::Projects, :api do
context 'when authenticated as project owner' do
it 'updates name' do
project_param = { name: 'bar' }
+
put api("/projects/#{project.id}", user), project_param
+
expect(response).to have_http_status(200)
+
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -1096,8 +1114,11 @@ describe API::Projects, :api do
it 'updates visibility_level' do
project_param = { visibility: 'public' }
+
put api("/projects/#{project3.id}", user), project_param
+
expect(response).to have_http_status(200)
+
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -1106,17 +1127,23 @@ describe API::Projects, :api do
it 'updates visibility_level from public to private' do
project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
project_param = { visibility: 'private' }
+
put api("/projects/#{project3.id}", user), project_param
+
expect(response).to have_http_status(200)
+
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
+
expect(json_response['visibility']).to eq('private')
end
it 'does not update name to existing name' do
project_param = { name: project3.name }
+
put api("/projects/#{project.id}", user), project_param
+
expect(response).to have_http_status(400)
expect(json_response['message']['name']).to eq(['has already been taken'])
end
@@ -1132,8 +1159,23 @@ describe API::Projects, :api do
it 'updates path & name to existing path & name in different namespace' do
project_param = { path: project4.path, name: project4.name }
+
put api("/projects/#{project3.id}", user), project_param
+
expect(response).to have_http_status(200)
+
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'updates jobs_enabled' do
+ project_param = { jobs_enabled: true }
+
+ put api("/projects/#{project3.id}", user), project_param
+
+ expect(response).to have_http_status(200)
+
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index f793c0db2f3..165ab389917 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1,12 +1,12 @@
require 'spec_helper'
-describe API::Users, api: true do
+describe API::Users, api: true do
include ApiHelpers
- let(:user) { create(:user) }
+ let(:user) { create(:user) }
let(:admin) { create(:admin) }
- let(:key) { create(:key, user: user) }
- let(:email) { create(:email, user: user) }
+ let(:key) { create(:key, user: user) }
+ let(:email) { create(:email, user: user) }
let(:omniauth_user) { create(:omniauth_user) }
let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
@@ -72,6 +72,12 @@ describe API::Users, api: true do
expect(json_response).to be_an Array
expect(json_response.first['username']).to eq(omniauth_user.username)
end
+
+ it "returns a 403 when non-admin user searches by external UID" do
+ get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", user)
+
+ expect(response).to have_http_status(403)
+ end
end
context "when admin" do
@@ -100,6 +106,27 @@ describe API::Users, api: true do
expect(json_response).to be_an Array
expect(json_response).to all(include('external' => true))
end
+
+ it "returns one user by external UID" do
+ get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['username']).to eq(omniauth_user.username)
+ end
+
+ it "returns 400 error if provider with no extern_uid" do
+ get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}", admin)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 error if provider with no extern_uid" do
+ get api("/users?provider=#{omniauth_user.identities.first.provider}", admin)
+
+ expect(response).to have_http_status(400)
+ end
end
end
@@ -129,7 +156,7 @@ describe API::Users, api: true do
end
describe "POST /users" do
- before{ admin }
+ before { admin }
it "creates user" do
expect do
@@ -214,9 +241,9 @@ describe API::Users, api: true do
it "does not create user with invalid email" do
post api('/users', admin),
- email: 'invalid email',
- password: 'password',
- name: 'test'
+ email: 'invalid email',
+ password: 'password',
+ name: 'test'
expect(response).to have_http_status(400)
end
@@ -242,12 +269,12 @@ describe API::Users, api: true do
it 'returns 400 error if user does not validate' do
post api('/users', admin),
- password: 'pass',
- email: 'test@example.com',
- username: 'test!',
- name: 'test',
- bio: 'g' * 256,
- projects_limit: -1
+ password: 'pass',
+ email: 'test@example.com',
+ username: 'test!',
+ name: 'test',
+ bio: 'g' * 256,
+ projects_limit: -1
expect(response).to have_http_status(400)
expect(json_response['message']['password']).
to eq(['is too short (minimum is 8 characters)'])
@@ -267,19 +294,19 @@ describe API::Users, api: true do
context 'with existing user' do
before do
post api('/users', admin),
- email: 'test@example.com',
- password: 'password',
- username: 'test',
- name: 'foo'
+ email: 'test@example.com',
+ password: 'password',
+ username: 'test',
+ name: 'foo'
end
it 'returns 409 conflict error if user with same email exists' do
expect do
post api('/users', admin),
- name: 'foo',
- email: 'test@example.com',
- password: 'password',
- username: 'foo'
+ name: 'foo',
+ email: 'test@example.com',
+ password: 'password',
+ username: 'foo'
end.to change { User.count }.by(0)
expect(response).to have_http_status(409)
expect(json_response['message']).to eq('Email has already been taken')
@@ -288,10 +315,10 @@ describe API::Users, api: true do
it 'returns 409 conflict error if same username exists' do
expect do
post api('/users', admin),
- name: 'foo',
- email: 'foo@example.com',
- password: 'password',
- username: 'test'
+ name: 'foo',
+ email: 'foo@example.com',
+ password: 'password',
+ username: 'test'
end.to change { User.count }.by(0)
expect(response).to have_http_status(409)
expect(json_response['message']).to eq('Username has already been taken')
@@ -416,12 +443,12 @@ describe API::Users, api: true do
it 'returns 400 error if user does not validate' do
put api("/users/#{user.id}", admin),
- password: 'pass',
- email: 'test@example.com',
- username: 'test!',
- name: 'test',
- bio: 'g' * 256,
- projects_limit: -1
+ password: 'pass',
+ email: 'test@example.com',
+ username: 'test!',
+ name: 'test',
+ bio: 'g' * 256,
+ projects_limit: -1
expect(response).to have_http_status(400)
expect(json_response['message']['password']).
to eq(['is too short (minimum is 8 characters)'])
@@ -488,7 +515,7 @@ describe API::Users, api: true do
key_attrs = attributes_for :key
expect do
post api("/users/#{user.id}/keys", admin), key_attrs
- end.to change{ user.keys.count }.by(1)
+ end.to change { user.keys.count }.by(1)
end
it "returns 400 for invalid ID" do
@@ -580,7 +607,7 @@ describe API::Users, api: true do
email_attrs = attributes_for :email
expect do
post api("/users/#{user.id}/emails", admin), email_attrs
- end.to change{ user.emails.count }.by(1)
+ end.to change { user.emails.count }.by(1)
end
it "returns a 400 for invalid ID" do
@@ -842,7 +869,7 @@ describe API::Users, api: true do
key_attrs = attributes_for :key
expect do
post api("/user/keys", user), key_attrs
- end.to change{ user.keys.count }.by(1)
+ end.to change { user.keys.count }.by(1)
expect(response).to have_http_status(201)
end
@@ -880,7 +907,7 @@ describe API::Users, api: true do
delete api("/user/keys/#{key.id}", user)
expect(response).to have_http_status(204)
- end.to change{user.keys.count}.by(-1)
+ end.to change { user.keys.count}.by(-1)
end
it "returns 404 if key ID not found" do
@@ -963,7 +990,7 @@ describe API::Users, api: true do
email_attrs = attributes_for :email
expect do
post api("/user/emails", user), email_attrs
- end.to change{ user.emails.count }.by(1)
+ end.to change { user.emails.count }.by(1)
expect(response).to have_http_status(201)
end
@@ -989,7 +1016,7 @@ describe API::Users, api: true do
delete api("/user/emails/#{email.id}", user)
expect(response).to have_http_status(204)
- end.to change{user.emails.count}.by(-1)
+ end.to change { user.emails.count}.by(-1)
end
it "returns 404 if email ID not found" do
@@ -1158,6 +1185,49 @@ describe API::Users, api: true do
end
end
+ context "user activities", :redis do
+ let!(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) }
+ let!(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) }
+
+ context 'last activity as normal user' do
+ it 'has no permission' do
+ get api("/user/activities", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'as admin' do
+ it 'returns the activities from the last 6 months' do
+ get api("/user/activities", admin)
+
+ expect(response).to include_pagination_headers
+ expect(json_response.size).to eq(1)
+
+ activity = json_response.last
+
+ expect(activity['username']).to eq(newly_active_user.username)
+ expect(activity['last_activity_on']).to eq(2.days.ago.to_date.to_s)
+ expect(activity['last_activity_at']).to eq(2.days.ago.to_date.to_s)
+ end
+
+ context 'passing a :from parameter' do
+ it 'returns the activities from the given date' do
+ get api("/user/activities?from=2000-1-1", admin)
+
+ expect(response).to include_pagination_headers
+ expect(json_response.size).to eq(2)
+
+ activity = json_response.first
+
+ expect(activity['username']).to eq(old_active_user.username)
+ expect(activity['last_activity_on']).to eq(Time.utc(2000, 1, 1).to_date.to_s)
+ expect(activity['last_activity_at']).to eq(Time.utc(2000, 1, 1).to_date.to_s)
+ end
+ end
+ end
+ end
+
describe 'GET /users/:user_id/impersonation_tokens' do
let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
index adba3a787aa..0a28cb9bddb 100644
--- a/spec/requests/api/v3/commits_spec.rb
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -485,8 +485,7 @@ describe API::V3::Commits, api: true do
post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown'
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically.
- A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.')
+ expect(json_response['message']).to include('Sorry, we cannot cherry-pick this commit automatically.')
end
it 'returns 400 if you are not allowed to push to the target branch' do
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
index 349fd6b3415..c45e2028e1d 100644
--- a/spec/requests/api/v3/files_spec.rb
+++ b/spec/requests/api/v3/files_spec.rb
@@ -129,7 +129,7 @@ describe API::V3::Files, api: true do
it "returns a 400 if editor fails to create file" do
allow_any_instance_of(Repository).to receive(:create_file).
- and_return(false)
+ and_raise(Repository::CommitError, 'Cannot create file')
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
@@ -229,8 +229,8 @@ describe API::V3::Files, api: true do
expect(response).to have_http_status(400)
end
- it "returns a 400 if fails to create file" do
- allow_any_instance_of(Repository).to receive(:delete_file).and_return(false)
+ it "returns a 400 if fails to delete file" do
+ allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file')
delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 006d6a6af1c..316742ff076 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -3,6 +3,7 @@ require "spec_helper"
describe 'Git HTTP requests', lib: true do
include GitHttpHelpers
include WorkhorseHelpers
+ include UserActivitiesHelpers
it "gives WWW-Authenticate hints" do
clone_get('doesnt/exist.git')
@@ -255,6 +256,14 @@ describe 'Git HTTP requests', lib: true do
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
+
+ it 'updates the user last activity', :redis do
+ expect(user_activity(user)).to be_nil
+
+ download(path, env) do |response|
+ expect(user_activity(user)).to be_present
+ end
+ end
end
context "when an oauth token is provided" do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 4baccacd448..a3de022d242 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -484,7 +484,7 @@ describe 'project routing' do
end
it 'to #list' do
- expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
+ expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.json')
end
end
diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb
index 3cc791bca50..7f1abecfafe 100644
--- a/spec/serializers/build_serializer_spec.rb
+++ b/spec/serializers/build_serializer_spec.rb
@@ -38,7 +38,7 @@ describe BuildSerializer do
expect(subject[:text]).to eq(status.text)
expect(subject[:label]).to eq(status.label)
expect(subject[:icon]).to eq(status.icon)
- expect(subject[:favicon]).to eq(status.favicon)
+ expect(subject[:favicon]).to eq("/assets/ci_favicons/#{status.favicon}.ico")
end
end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index f6249ab4664..ecde45a6d44 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -144,7 +144,7 @@ describe PipelineSerializer do
expect(subject[:text]).to eq(status.text)
expect(subject[:label]).to eq(status.label)
expect(subject[:icon]).to eq(status.icon)
- expect(subject[:favicon]).to eq(status.favicon)
+ expect(subject[:favicon]).to eq("/assets/ci_favicons/#{status.favicon}.ico")
end
end
end
diff --git a/spec/services/cohorts_service_spec.rb b/spec/services/cohorts_service_spec.rb
new file mode 100644
index 00000000000..1e99442fdcb
--- /dev/null
+++ b/spec/services/cohorts_service_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe CohortsService do
+ describe '#execute' do
+ def month_start(months_ago)
+ months_ago.months.ago.beginning_of_month.to_date
+ end
+
+ # In the interests of speed and clarity, this example has minimal data.
+ it 'returns a list of user cohorts' do
+ 6.times do |months_ago|
+ months_ago_time = (months_ago * 2).months.ago
+
+ create(:user, created_at: months_ago_time, last_activity_on: Time.now)
+ create(:user, created_at: months_ago_time, last_activity_on: months_ago_time)
+ end
+
+ create(:user) # this user is inactive and belongs to the current month
+
+ expected_cohorts = [
+ {
+ registration_month: month_start(11),
+ activity_months: Array.new(12) { { total: 0, percentage: 0 } },
+ total: 0,
+ inactive: 0
+ },
+ {
+ registration_month: month_start(10),
+ activity_months: [{ total: 2, percentage: 100 }] + Array.new(10) { { total: 1, percentage: 50 } },
+ total: 2,
+ inactive: 0
+ },
+ {
+ registration_month: month_start(9),
+ activity_months: Array.new(10) { { total: 0, percentage: 0 } },
+ total: 0,
+ inactive: 0
+ },
+ {
+ registration_month: month_start(8),
+ activity_months: [{ total: 2, percentage: 100 }] + Array.new(8) { { total: 1, percentage: 50 } },
+ total: 2,
+ inactive: 0
+ },
+ {
+ registration_month: month_start(7),
+ activity_months: Array.new(8) { { total: 0, percentage: 0 } },
+ total: 0,
+ inactive: 0
+ },
+ {
+ registration_month: month_start(6),
+ activity_months: [{ total: 2, percentage: 100 }] + Array.new(6) { { total: 1, percentage: 50 } },
+ total: 2,
+ inactive: 0
+ },
+ {
+ registration_month: month_start(5),
+ activity_months: Array.new(6) { { total: 0, percentage: 0 } },
+ total: 0,
+ inactive: 0
+ },
+ {
+ registration_month: month_start(4),
+ activity_months: [{ total: 2, percentage: 100 }] + Array.new(4) { { total: 1, percentage: 50 } },
+ total: 2,
+ inactive: 0
+ },
+ {
+ registration_month: month_start(3),
+ activity_months: Array.new(4) { { total: 0, percentage: 0 } },
+ total: 0,
+ inactive: 0
+ },
+ {
+ registration_month: month_start(2),
+ activity_months: [{ total: 2, percentage: 100 }] + Array.new(2) { { total: 1, percentage: 50 } },
+ total: 2,
+ inactive: 0
+ },
+ {
+ registration_month: month_start(1),
+ activity_months: Array.new(2) { { total: 0, percentage: 0 } },
+ total: 0,
+ inactive: 0
+ },
+ {
+ registration_month: month_start(0),
+ activity_months: [{ total: 2, percentage: 100 }],
+ total: 2,
+ inactive: 1
+ },
+ ]
+
+ expect(described_class.new.execute).to eq(months_included: 12,
+ cohorts: expected_cohorts)
+ end
+ end
+end
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
index a41a421fa6e..7b921f606f8 100644
--- a/spec/services/delete_merged_branches_service_spec.rb
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -42,6 +42,19 @@ describe DeleteMergedBranchesService, services: true do
expect { described_class.new(project, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
+
+ context 'open merge requests' do
+ it 'does not delete branches from open merge requests' do
+ fork_link = create(:forked_project_link, forked_from_project: project)
+ create(:merge_request, :reopened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master')
+ create(:merge_request, :opened, source_project: fork_link.forked_to_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master')
+
+ service.execute
+
+ expect(project.repository.branch_names).to include('branch-merged')
+ expect(project.repository.branch_names).to include('improve/awesome')
+ end
+ end
end
context '#async_execute' do
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index f2c2009bcbf..b06cefe071d 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe EventCreateService, services: true do
+ include UserActivitiesHelpers
+
let(:service) { EventCreateService.new }
describe 'Issues' do
@@ -111,6 +113,19 @@ describe EventCreateService, services: true do
end
end
+ describe '#push', :redis do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ it 'creates a new event' do
+ expect { service.push(project, user, {}) }.to change { Event.count }
+ end
+
+ it 'updates user last activity' do
+ expect { service.push(project, user, {}) }.to change { user_activity(user) }
+ end
+ end
+
describe 'Project' do
let(:user) { create :user }
let(:project) { create(:empty_project) }
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index 26aa5b432d4..16bca66766a 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -7,7 +7,7 @@ describe Files::UpdateService do
let(:user) { create(:user) }
let(:file_path) { 'files/ruby/popen.rb' }
let(:new_contents) { 'New Content' }
- let(:target_branch) { project.default_branch }
+ let(:branch_name) { project.default_branch }
let(:last_commit_sha) { nil }
let(:commit_params) do
@@ -19,7 +19,7 @@ describe Files::UpdateService do
last_commit_sha: last_commit_sha,
start_project: project,
start_branch: project.default_branch,
- target_branch: target_branch
+ branch_name: branch_name
}
end
@@ -73,7 +73,7 @@ describe Files::UpdateService do
end
context 'when target branch is different than source branch' do
- let(:target_branch) { "#{project.default_branch}-new" }
+ let(:branch_name) { "#{project.default_branch}-new" }
it 'fires hooks only once' do
expect(GitHooksService).to receive(:new).once.and_call_original
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 2ee11fc8b4c..a37257d1bf4 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -7,6 +7,7 @@ describe Groups::DestroyService, services: true do
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:empty_project, namespace: group) }
+ let!(:notification_setting) { create(:notification_setting, source: group)}
let!(:gitlab_shell) { Gitlab::Shell.new }
let!(:remove_path) { group.path + "+#{group.id}+deleted" }
@@ -23,6 +24,7 @@ describe Groups::DestroyService, services: true do
it { expect(Group.unscoped.all).not_to include(group) }
it { expect(Group.unscoped.all).not_to include(nested_group) }
it { expect(Project.unscoped.all).not_to include(project) }
+ it { expect(NotificationSetting.unscoped.all).not_to include(notification_setting) }
end
context 'file system' do
diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb
new file mode 100644
index 00000000000..3b35a3b8e3a
--- /dev/null
+++ b/spec/services/members/authorized_destroy_service_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Members::AuthorizedDestroyService, services: true do
+ let(:member_user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+ let(:group) { create(:group, :public) }
+ let(:group_project) { create(:empty_project, :public, group: group) }
+
+ def number_of_assigned_issuables(user)
+ Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count
+ end
+
+ context 'Group member' do
+ it "unassigns issues and merge requests" do
+ group.add_developer(member_user)
+
+ issue = create :issue, project: group_project, assignee: member_user
+ create :issue, assignee: member_user
+ merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user
+ create :merge_request, target_project: project, source_project: project, assignee: member_user
+
+ member = group.members.find_by(user_id: member_user.id)
+
+ expect { described_class.new(member, member_user).execute }
+ .to change { number_of_assigned_issuables(member_user) }.from(4).to(2)
+
+ expect(issue.reload.assignee_id).to be_nil
+ expect(merge_request.reload.assignee_id).to be_nil
+ end
+ end
+
+ context 'Project member' do
+ it "unassigns issues and merge requests" do
+ project.team << [member_user, :developer]
+
+ create :issue, project: project, assignee: member_user
+ create :merge_request, target_project: project, source_project: project, assignee: member_user
+
+ member = project.members.find_by(user_id: member_user.id)
+
+ expect { described_class.new(member, member_user).execute }
+ .to change { number_of_assigned_issuables(member_user) }.from(2).to(0)
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 617c8eaf3d5..989fd90cda9 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -1181,6 +1181,22 @@ describe NotificationService, services: true do
should_not_email(@u_disabled)
end
end
+
+ describe '#project_exported' do
+ it do
+ notification.project_exported(project, @u_disabled)
+
+ should_only_email(@u_disabled)
+ end
+ end
+
+ describe '#project_not_exported' do
+ it do
+ notification.project_not_exported(project, @u_disabled, ['error'])
+
+ should_only_email(@u_disabled)
+ end
+ end
end
describe 'GroupMember' do
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 62f21049b0b..7a07ea618c0 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -144,6 +144,20 @@ describe Projects::CreateService, '#execute', services: true do
end
end
+ context 'when a bad service template is created' do
+ before do
+ create(:service, type: 'DroneCiService', project: nil, template: true, active: true)
+ end
+
+ it 'reports an error in the imported project' do
+ opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-ce'
+ project = create_project(user, opts)
+
+ expect(project.errors.full_messages_for(:base).first).to match /Unable to save project. Error: Unable to save DroneCiService/
+ expect(project.services.count).to eq 0
+ end
+ end
+
def create_project(user, opts)
Projects::CreateService.new(user, opts).execute
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 09cfa36b3b9..852a4ac852f 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -54,6 +54,15 @@ describe Projects::ImportService, services: true do
expect(result[:status]).to eq :error
expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository"
end
+
+ it 'does not remove the GitHub remote' do
+ expect_any_instance_of(Repository).to receive(:fetch_remote).and_return(true)
+ expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true)
+
+ subject.execute
+
+ expect(project.repository.raw_repository.remote_names).to include('github')
+ end
end
context 'with a non Github repository' do
diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb
index 2531607acad..cbf4f56213d 100644
--- a/spec/services/search/global_service_spec.rb
+++ b/spec/services/search/global_service_spec.rb
@@ -40,27 +40,6 @@ describe Search::GlobalService, services: true do
expect(results.objects('projects')).to match_array [found_project]
end
-
- context 'nested group' do
- let!(:nested_group) { create(:group, :nested) }
- let!(:project) { create(:empty_project, namespace: nested_group) }
-
- before do
- project.add_master(user)
- end
-
- it 'returns result from nested group' do
- results = Search::GlobalService.new(user, search: project.path).execute
-
- expect(results.objects('projects')).to match_array [project]
- end
-
- it 'returns result from descendants when search inside group' do
- results = Search::GlobalService.new(user, search: project.path, group_id: nested_group.parent).execute
-
- expect(results.objects('projects')).to match_array [project]
- end
- end
end
end
end
diff --git a/spec/services/search/group_service_spec.rb b/spec/services/search/group_service_spec.rb
new file mode 100644
index 00000000000..38f264f6e7b
--- /dev/null
+++ b/spec/services/search/group_service_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Search::GroupService, services: true do
+ shared_examples_for 'group search' do
+ context 'finding projects by name' do
+ let(:user) { create(:user) }
+ let(:term) { "Project Name" }
+ let(:nested_group) { create(:group, :nested) }
+
+ # These projects shouldn't be found
+ let!(:outside_project) { create(:empty_project, :public, name: "Outside #{term}") }
+ let!(:private_project) { create(:empty_project, :private, namespace: nested_group, name: "Private #{term}" )}
+ let!(:other_project) { create(:empty_project, :public, namespace: nested_group, name: term.reverse) }
+
+ # These projects should be found
+ let!(:project1) { create(:empty_project, :internal, namespace: nested_group, name: "Inner #{term} 1") }
+ let!(:project2) { create(:empty_project, :internal, namespace: nested_group, name: "Inner #{term} 2") }
+ let!(:project3) { create(:empty_project, :internal, namespace: nested_group.parent, name: "Outer #{term}") }
+
+ let(:results) { Search::GroupService.new(user, search_group, search: term).execute }
+ subject { results.objects('projects') }
+
+ context 'in parent group' do
+ let(:search_group) { nested_group.parent }
+
+ it { is_expected.to match_array([project1, project2, project3]) }
+ end
+
+ context 'in subgroup' do
+ let(:search_group) { nested_group }
+
+ it { is_expected.to match_array([project1, project2]) }
+ end
+ end
+ end
+
+ describe 'basic search' do
+ include_examples 'group search'
+ end
+end
diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb
new file mode 100644
index 00000000000..8d67ebe3231
--- /dev/null
+++ b/spec/services/users/activity_service_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Users::ActivityService, services: true do
+ include UserActivitiesHelpers
+
+ let(:user) { create(:user) }
+
+ subject(:service) { described_class.new(user, 'type') }
+
+ describe '#execute', :redis do
+ context 'when last activity is nil' do
+ before do
+ service.execute
+ end
+
+ it 'sets the last activity timestamp for the user' do
+ expect(last_hour_user_ids).to eq([user.id])
+ end
+
+ it 'updates the same user' do
+ service.execute
+
+ expect(last_hour_user_ids).to eq([user.id])
+ end
+
+ it 'updates the timestamp of an existing user' do
+ Timecop.freeze(Date.tomorrow) do
+ expect { service.execute }.to change { user_activity(user) }.to(Time.now.to_i.to_s)
+ end
+ end
+
+ describe 'other user' do
+ it 'updates other user' do
+ other_user = create(:user)
+ described_class.new(other_user, 'type').execute
+
+ expect(last_hour_user_ids).to match_array([user.id, other_user.id])
+ end
+ end
+ end
+ end
+
+ def last_hour_user_ids
+ Gitlab::UserActivities.new.
+ select { |k, v| v >= 1.hour.ago.to_i.to_s }.
+ map { |k, _| k.to_i }
+ end
+end
diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb
new file mode 100644
index 00000000000..2a6bfc1b3a0
--- /dev/null
+++ b/spec/services/users/build_service_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Users::BuildService, services: true do
+ describe '#execute' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' }
+ end
+
+ context 'with an admin user' do
+ let(:admin_user) { create(:admin) }
+ let(:service) { described_class.new(admin_user, params) }
+
+ it 'returns a valid user' do
+ expect(service.execute).to be_valid
+ end
+ end
+
+ context 'with non admin user' do
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(user, params) }
+
+ it 'raises AccessDeniedError exception' do
+ expect { service.execute }.to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+
+ context 'with nil user' do
+ let(:service) { described_class.new(nil, params) }
+
+ it 'returns a valid user' do
+ expect(service.execute).to be_valid
+ end
+
+ context 'when "send_user_confirmation_email" application setting is true' do
+ before do
+ stub_application_setting(send_user_confirmation_email: true, signup_enabled?: true)
+ end
+
+ it 'does not confirm the user' do
+ expect(service.execute).not_to be_confirmed
+ end
+ end
+
+ context 'when "send_user_confirmation_email" application setting is false' do
+ before do
+ stub_application_setting(send_user_confirmation_email: false, signup_enabled?: true)
+ end
+
+ it 'confirms the user' do
+ expect(service.execute).to be_confirmed
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb
index a111aec2f89..75746278573 100644
--- a/spec/services/users/create_service_spec.rb
+++ b/spec/services/users/create_service_spec.rb
@@ -1,38 +1,6 @@
require 'spec_helper'
describe Users::CreateService, services: true do
- describe '#build' do
- let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' }
- end
-
- context 'with an admin user' do
- let(:admin_user) { create(:admin) }
- let(:service) { described_class.new(admin_user, params) }
-
- it 'returns a valid user' do
- expect(service.build).to be_valid
- end
- end
-
- context 'with non admin user' do
- let(:user) { create(:user) }
- let(:service) { described_class.new(user, params) }
-
- it 'raises AccessDeniedError exception' do
- expect { service.build }.to raise_error Gitlab::Access::AccessDeniedError
- end
- end
-
- context 'with nil user' do
- let(:service) { described_class.new(nil, params) }
-
- it 'returns a valid user' do
- expect(service.build).to be_valid
- end
- end
- end
-
describe '#execute' do
let(:admin_user) { create(:admin) }
@@ -185,40 +153,18 @@ describe Users::CreateService, services: true do
end
let(:service) { described_class.new(nil, params) }
- context 'when "send_user_confirmation_email" application setting is true' do
- before do
- current_application_settings = double(:current_application_settings, send_user_confirmation_email: true, signup_enabled?: true)
- allow(service).to receive(:current_application_settings).and_return(current_application_settings)
- end
-
- it 'does not confirm the user' do
- expect(service.execute).not_to be_confirmed
- end
- end
-
- context 'when "send_user_confirmation_email" application setting is false' do
- before do
- current_application_settings = double(:current_application_settings, send_user_confirmation_email: false, signup_enabled?: true)
- allow(service).to receive(:current_application_settings).and_return(current_application_settings)
- end
-
- it 'confirms the user' do
- expect(service.execute).to be_confirmed
- end
-
- it 'persists the given attributes' do
- user = service.execute
- user.reload
-
- expect(user).to have_attributes(
- name: params[:name],
- username: params[:username],
- email: params[:email],
- password: params[:password],
- created_by_id: nil,
- admin: false
- )
- end
+ it 'persists the given attributes' do
+ user = service.execute
+ user.reload
+
+ expect(user).to have_attributes(
+ name: params[:name],
+ username: params[:username],
+ email: params[:email],
+ password: params[:password],
+ created_by_id: nil,
+ admin: false
+ )
end
end
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 43c18992d1a..4bc30018ebd 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -152,6 +152,12 @@ describe Users::DestroyService, services: true do
service.execute(user)
end
+
+ it 'does not run `MigrateToGhostUser` if hard_delete option is given' do
+ expect_any_instance_of(Users::MigrateToGhostUserService).not_to receive(:execute)
+
+ service.execute(user, hard_delete: true)
+ end
end
end
end
diff --git a/spec/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb
index 8c5b7e41c15..9e1edf1ac30 100644
--- a/spec/services/users/migrate_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_to_ghost_user_service_spec.rb
@@ -60,5 +60,23 @@ describe Users::MigrateToGhostUserService, services: true do
end
end
end
+
+ context "when record migration fails with a rollback exception" do
+ before do
+ expect_any_instance_of(MergeRequest::ActiveRecord_Associations_CollectionProxy)
+ .to receive(:update_all).and_raise(ActiveRecord::Rollback)
+ end
+
+ context "for records that were already migrated" do
+ let!(:issue) { create(:issue, project: project, author: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, target_branch: "first") }
+
+ it "reverses the migration" do
+ service.execute
+
+ expect(issue.reload.author).to eq(user)
+ end
+ end
+ end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 4eb5b150af5..e67ad8f3455 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -9,8 +9,14 @@ require 'rspec/rails'
require 'shoulda/matchers'
require 'rspec/retry'
-if (ENV['RSPEC_PROFILING_POSTGRES_URL'] || ENV['RSPEC_PROFILING']) &&
- (!ENV.has_key?('CI') || ENV['CI_COMMIT_REF_NAME'] == 'master')
+rspec_profiling_is_configured =
+ ENV['RSPEC_PROFILING_POSTGRES_URL'] ||
+ ENV['RSPEC_PROFILING']
+branch_can_be_profiled =
+ ENV['CI_COMMIT_REF_NAME'] == 'master' ||
+ ENV['CI_COMMIT_REF_NAME'] =~ /rspec-profile/
+
+if rspec_profiling_is_configured && (!ENV.key?('CI') || branch_can_be_profiled)
require 'rspec_profiling/rspec'
end
@@ -59,6 +65,10 @@ RSpec.configure do |config|
TestEnv.init
end
+ config.after(:suite) do
+ TestEnv.cleanup
+ end
+
if ENV['CI']
# Retry only on feature specs that use JS
config.around :each, :js do |ex|
diff --git a/spec/features/discussion_comments_spec.rb b/spec/support/features/discussion_comments_shared_example.rb
index ae778118c5c..bb4542b1683 100644
--- a/spec/features/discussion_comments_spec.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
shared_examples 'discussion comments' do |resource_name|
let(:form_selector) { '.js-main-target-form' }
let(:dropdown_selector) { "#{form_selector} .comment-type-dropdown" }
@@ -9,11 +7,9 @@ shared_examples 'discussion comments' do |resource_name|
let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
let(:comments_selector) { '.timeline > .note.timeline-entry' }
- it 'should show a comment type toggle' do
+ it 'clicking "Comment" will post a comment' do
expect(page).to have_selector toggle_selector
- end
- it 'clicking "Comment" will post a comment' do
find("#{form_selector} .note-textarea").send_keys('a')
find(submit_selector).click
@@ -49,52 +45,43 @@ shared_examples 'discussion comments' do |resource_name|
find(toggle_selector).click
end
- it 'opens a comment type dropdown with "Comment" and "Start discussion"' do
+ it 'has a "Comment" item (selected by default) and "Start discussion" item' do
expect(page).to have_selector menu_selector
- end
-
- it 'has a "Comment" item' do
- menu = find(menu_selector)
-
- expect(menu).to have_content 'Comment'
- expect(menu).to have_content "Add a general comment to this #{resource_name}."
- end
- it 'has a "Start discussion" item' do
- menu = find(menu_selector)
-
- expect(menu).to have_content 'Start discussion'
- expect(menu).to have_content "Discuss a specific suggestion or question#{' that needs to be resolved' if resource_name == 'merge request'}."
- end
-
- it 'has the "Comment" item selected by default' do
find("#{menu_selector} li", match: :first)
items = all("#{menu_selector} li")
expect(items.first).to have_content 'Comment'
+ expect(items.first).to have_content "Add a general comment to this #{resource_name}."
expect(items.first).to have_selector '.fa-check'
expect(items.first['class']).to match 'droplab-item-selected'
expect(items.last).to have_content 'Start discussion'
+ expect(items.last).to have_content "Discuss a specific suggestion or question#{' that needs to be resolved' if resource_name == 'merge request'}."
expect(items.last).not_to have_selector '.fa-check'
expect(items.last['class']).not_to match 'droplab-item-selected'
end
- it 'closes the menu when clicking the toggle' do
+ it 'closes the menu when clicking the toggle or body' do
find(toggle_selector).click
expect(page).not_to have_selector menu_selector
- end
- it 'closes the menu when clicking the body' do
+ find(toggle_selector).click
find('body').click
expect(page).not_to have_selector menu_selector
end
- it 'clicking the ul padding should not change the text' do
+ it 'clicking the ul padding or divider should not change the text' do
find(menu_selector).trigger 'click'
+ expect(page).to have_selector menu_selector
+ expect(find(dropdown_selector)).to have_content 'Comment'
+
+ find("#{menu_selector} .divider").trigger 'click'
+
+ expect(page).to have_selector menu_selector
expect(find(dropdown_selector)).to have_content 'Comment'
end
@@ -104,12 +91,10 @@ shared_examples 'discussion comments' do |resource_name|
all("#{menu_selector} li").last.click
end
- it 'updates the note_type input to "DiscussionNote"' do
- expect(find("#{form_selector} #note_type", visible: false).value).to eq('DiscussionNote')
- end
-
- it 'updates the submit button text' do
+ it 'updates the submit button text, note_type input and closes the dropdown' do
expect(find(dropdown_selector)).to have_content 'Start discussion'
+ expect(find("#{form_selector} #note_type", visible: false).value).to eq('DiscussionNote')
+ expect(page).not_to have_selector menu_selector
end
if resource_name =~ /(issue|merge request)/
@@ -124,10 +109,6 @@ shared_examples 'discussion comments' do |resource_name|
end
end
- it 'closes the dropdown' do
- expect(page).not_to have_selector menu_selector
- end
-
it 'clicking "Start discussion" will post a discussion' do
find(submit_selector).click
@@ -176,12 +157,10 @@ shared_examples 'discussion comments' do |resource_name|
find("#{menu_selector} li", match: :first).click
end
- it 'clears the note_type input"' do
- expect(find("#{form_selector} #note_type", visible: false).value).to eq('')
- end
-
- it 'updates the submit button text' do
+ it 'updates the submit button text, clears the note_type input and closes the dropdown' do
expect(find(dropdown_selector)).to have_content 'Comment'
+ expect(find("#{form_selector} #note_type", visible: false).value).to eq('')
+ expect(page).not_to have_selector menu_selector
end
if resource_name =~ /(issue|merge request)/
@@ -196,10 +175,6 @@ shared_examples 'discussion comments' do |resource_name|
end
end
- it 'closes the dropdown' do
- expect(page).not_to have_selector menu_selector
- end
-
it 'should have "Comment" selected when opening the menu' do
find(toggle_selector).click
@@ -242,54 +217,3 @@ shared_examples 'discussion comments' do |resource_name|
end
end
end
-
-describe 'Discussion Comments', :feature, :js do
- include RepoHelpers
-
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
- before do
- project.team << [user, :developer]
-
- login_as(user)
- end
-
- describe 'on a merge request' do
- let(:merge_request) { create(:merge_request, source_project: project) }
-
- before do
- visit namespace_project_merge_request_path(project.namespace, project, merge_request)
- end
-
- it_behaves_like 'discussion comments', 'merge request'
- end
-
- describe 'on an issue' do
- let(:issue) { create(:issue, project: project) }
-
- before do
- visit namespace_project_issue_path(project.namespace, project, issue)
- end
-
- it_behaves_like 'discussion comments', 'issue'
- end
-
- describe 'on an snippet' do
- let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
-
- before do
- visit namespace_project_snippet_path(project.namespace, project, snippet)
- end
-
- it_behaves_like 'discussion comments', 'snippet'
- end
-
- describe 'on a commit' do
- before do
- visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
- end
-
- it_behaves_like 'discussion comments', 'commit'
- end
-end
diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb
index a05c9d18002..5515c355cea 100644
--- a/spec/support/fixture_helpers.rb
+++ b/spec/support/fixture_helpers.rb
@@ -1,8 +1,11 @@
module FixtureHelpers
def fixture_file(filename)
return '' if filename.blank?
- file_path = File.expand_path(Rails.root.join('spec/fixtures/', filename))
- File.read(file_path)
+ File.read(expand_fixture_path(filename))
+ end
+
+ def expand_fixture_path(filename)
+ File.expand_path(Rails.root.join('spec/fixtures/', filename))
end
end
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
new file mode 100644
index 00000000000..7aca902fc61
--- /dev/null
+++ b/spec/support/gitaly.rb
@@ -0,0 +1,7 @@
+if Gitlab::GitalyClient.enabled?
+ RSpec.configure do |config|
+ config.before(:each) do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+ end
+ end
+end
diff --git a/spec/support/matchers/user_activity_matchers.rb b/spec/support/matchers/user_activity_matchers.rb
new file mode 100644
index 00000000000..ce3b683b6d2
--- /dev/null
+++ b/spec/support/matchers/user_activity_matchers.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :have_an_activity_record do |expected|
+ match do |user|
+ expect(Gitlab::UserActivities.new.find { |k, _| k == user.id.to_s }).to be_present
+ end
+end
diff --git a/spec/support/mobile_helpers.rb b/spec/support/mobile_helpers.rb
index 20d5849bcab..431f20a2a5c 100644
--- a/spec/support/mobile_helpers.rb
+++ b/spec/support/mobile_helpers.rb
@@ -1,4 +1,8 @@
module MobileHelpers
+ def resize_screen_xs
+ resize_window(767, 768)
+ end
+
def resize_screen_sm
resize_window(900, 768)
end
diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
index 0eac587e973..dcc562c684b 100644
--- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
+++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
@@ -35,5 +35,57 @@ shared_examples "migrating a deleted user's associated records to the ghost user
expect(user).to be_blocked
end
+
+ context "race conditions" do
+ context "when #{record_class_name} migration fails and is rolled back" do
+ before do
+ expect_any_instance_of(record_class::ActiveRecord_Associations_CollectionProxy)
+ .to receive(:update_all).and_raise(ActiveRecord::Rollback)
+ end
+
+ it 'rolls back the user block' do
+ service.execute
+
+ expect(user.reload).not_to be_blocked
+ end
+
+ it "doesn't unblock an previously-blocked user" do
+ user.block
+
+ service.execute
+
+ expect(user.reload).to be_blocked
+ end
+ end
+
+ context "when #{record_class_name} migration fails with a non-rollback exception" do
+ before do
+ expect_any_instance_of(record_class::ActiveRecord_Associations_CollectionProxy)
+ .to receive(:update_all).and_raise(ArgumentError)
+ end
+
+ it 'rolls back the user block' do
+ service.execute rescue nil
+
+ expect(user.reload).not_to be_blocked
+ end
+
+ it "doesn't unblock an previously-blocked user" do
+ user.block
+
+ service.execute rescue nil
+
+ expect(user.reload).to be_blocked
+ end
+ end
+
+ it "blocks the user before #{record_class_name} migration begins" do
+ expect(service).to receive("migrate_#{record_class_name.parameterize('_')}s".to_sym) do
+ expect(user.reload).to be_blocked
+ end
+
+ service.execute
+ end
+ end
end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 1b5cb71a6b0..60c2096a126 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -64,6 +64,8 @@ module TestEnv
# Setup GitLab shell for test instance
setup_gitlab_shell
+ setup_gitaly if Gitlab::GitalyClient.enabled?
+
# Create repository for FactoryGirl.create(:project)
setup_factory_repo
@@ -71,6 +73,10 @@ module TestEnv
setup_forked_repo
end
+ def cleanup
+ stop_gitaly
+ end
+
def disable_mailer
allow_any_instance_of(NotificationService).to receive(:mailer).
and_return(double.as_null_object)
@@ -92,7 +98,7 @@ module TestEnv
tmp_test_path = Rails.root.join('tmp', 'tests', '**')
Dir[tmp_test_path].each do |entry|
- unless File.basename(entry) =~ /\Agitlab-(shell|test|test_bare|test-fork|test-fork_bare)\z/
+ unless File.basename(entry) =~ /\A(gitaly|gitlab-(shell|test|test_bare|test-fork|test-fork_bare))\z/
FileUtils.rm_rf(entry)
end
end
@@ -110,6 +116,28 @@ module TestEnv
end
end
+ def setup_gitaly
+ socket_path = Gitlab::GitalyClient.get_address('default').sub(/\Aunix:/, '')
+ gitaly_dir = File.dirname(socket_path)
+
+ unless File.directory?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
+ raise "Can't clone gitaly"
+ end
+
+ start_gitaly(gitaly_dir, socket_path)
+ end
+
+ def start_gitaly(gitaly_dir, socket_path)
+ gitaly_exec = File.join(gitaly_dir, 'gitaly')
+ @gitaly_pid = spawn({ "GITALY_SOCKET_PATH" => socket_path }, gitaly_exec, [:out, :err] => '/dev/null')
+ end
+
+ def stop_gitaly
+ return unless @gitaly_pid
+
+ Process.kill('KILL', @gitaly_pid)
+ end
+
def setup_factory_repo
setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name,
BRANCH_SHA)
diff --git a/spec/support/user_activities_helpers.rb b/spec/support/user_activities_helpers.rb
new file mode 100644
index 00000000000..f7ca9a31edd
--- /dev/null
+++ b/spec/support/user_activities_helpers.rb
@@ -0,0 +1,7 @@
+module UserActivitiesHelpers
+ def user_activity(user)
+ Gitlab::UserActivities.new.
+ find { |k, _| k == user.id.to_s }&.
+ second
+ end
+end
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index b369dcbb305..aaf998a546f 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -8,7 +8,7 @@ describe 'gitlab:gitaly namespace rake task' do
describe 'install' do
let(:repo) { 'https://gitlab.com/gitlab-org/gitaly.git' }
let(:clone_path) { Rails.root.join('tmp/tests/gitaly').to_s }
- let(:tag) { "v#{File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp}" }
+ let(:version) { File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp }
context 'no dir given' do
it 'aborts and display a help message' do
@@ -21,7 +21,7 @@ describe 'gitlab:gitaly namespace rake task' do
context 'when an underlying Git command fail' do
it 'aborts and display a help message' do
expect_any_instance_of(Object).
- to receive(:checkout_or_clone_tag).and_raise 'Git error'
+ to receive(:checkout_or_clone_version).and_raise 'Git error'
expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error 'Git error'
end
@@ -32,9 +32,9 @@ describe 'gitlab:gitaly namespace rake task' do
expect(Dir).to receive(:chdir).with(clone_path)
end
- it 'calls checkout_or_clone_tag with the right arguments' do
+ it 'calls checkout_or_clone_version with the right arguments' do
expect_any_instance_of(Object).
- to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path)
+ to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path)
run_rake_task('gitlab:gitaly:install', clone_path)
end
@@ -48,7 +48,7 @@ describe 'gitlab:gitaly namespace rake task' do
context 'gmake is available' do
before do
- expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
+ expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
end
@@ -62,7 +62,7 @@ describe 'gitlab:gitaly namespace rake task' do
context 'gmake is not available' do
before do
- expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
+ expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
end
diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb
index 86e42d845ce..3d9ba7cdc6f 100644
--- a/spec/tasks/gitlab/task_helpers_spec.rb
+++ b/spec/tasks/gitlab/task_helpers_spec.rb
@@ -10,19 +10,38 @@ describe Gitlab::TaskHelpers do
let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-test.git' }
let(:clone_path) { Rails.root.join('tmp/tests/task_helpers_tests').to_s }
+ let(:version) { '1.1.0' }
let(:tag) { 'v1.1.0' }
- describe '#checkout_or_clone_tag' do
+ describe '#checkout_or_clone_version' do
before do
allow(subject).to receive(:run_command!)
- expect(subject).to receive(:reset_to_tag).with(tag, clone_path)
end
- context 'target_dir does not exist' do
- it 'clones the repo, retrieve the tag from origin, and checkout the tag' do
+ it 'checkout the version and reset to it' do
+ expect(subject).to receive(:checkout_version).with(tag, clone_path)
+ expect(subject).to receive(:reset_to_version).with(tag, clone_path)
+
+ subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
+ end
+
+ context 'with a branch version' do
+ let(:version) { '=branch_name' }
+ let(:branch) { 'branch_name' }
+
+ it 'checkout the version and reset to it with a branch name' do
+ expect(subject).to receive(:checkout_version).with(branch, clone_path)
+ expect(subject).to receive(:reset_to_version).with(branch, clone_path)
+
+ subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
+ end
+ end
+
+ context "target_dir doesn't exist" do
+ it 'clones the repo' do
expect(subject).to receive(:clone_repo).with(repo, clone_path)
- subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path)
+ subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
end
end
@@ -31,10 +50,10 @@ describe Gitlab::TaskHelpers do
expect(Dir).to receive(:exist?).and_return(true)
end
- it 'fetch and checkout the tag' do
- expect(subject).to receive(:checkout_tag).with(tag, clone_path)
+ it "doesn't clone the repository" do
+ expect(subject).not_to receive(:clone_repo)
- subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path)
+ subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
end
end
end
@@ -48,49 +67,23 @@ describe Gitlab::TaskHelpers do
end
end
- describe '#checkout_tag' do
+ describe '#checkout_version' do
it 'clones the repo in the target dir' do
expect(subject).
- to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --tags --quiet])
+ to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --quiet])
expect(subject).
to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} checkout --quiet #{tag}])
- subject.checkout_tag(tag, clone_path)
+ subject.checkout_version(tag, clone_path)
end
end
- describe '#reset_to_tag' do
- let(:tag) { 'v1.1.0' }
- before do
+ describe '#reset_to_version' do
+ it 'resets --hard to the given version' do
expect(subject).
to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} reset --hard #{tag}])
- end
- context 'when the tag is not checked out locally' do
- before do
- expect(subject).
- to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_raise(Gitlab::TaskFailedError)
- end
-
- it 'fetch origin, ensure the tag exists, and resets --hard to the given tag' do
- expect(subject).
- to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch origin])
- expect(subject).
- to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- origin/#{tag}]).and_return(tag)
-
- subject.reset_to_tag(tag, clone_path)
- end
- end
-
- context 'when the tag is checked out locally' do
- before do
- expect(subject).
- to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_return(tag)
- end
-
- it 'resets --hard to the given tag' do
- subject.reset_to_tag(tag, clone_path)
- end
+ subject.reset_to_version(tag, clone_path)
end
end
end
diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb
index 8a66a4aa047..63d1cf2bbe5 100644
--- a/spec/tasks/gitlab/workhorse_rake_spec.rb
+++ b/spec/tasks/gitlab/workhorse_rake_spec.rb
@@ -8,7 +8,7 @@ describe 'gitlab:workhorse namespace rake task' do
describe 'install' do
let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' }
let(:clone_path) { Rails.root.join('tmp/tests/gitlab-workhorse').to_s }
- let(:tag) { "v#{File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp}" }
+ let(:version) { File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp }
context 'no dir given' do
it 'aborts and display a help message' do
@@ -21,7 +21,7 @@ describe 'gitlab:workhorse namespace rake task' do
context 'when an underlying Git command fail' do
it 'aborts and display a help message' do
expect_any_instance_of(Object).
- to receive(:checkout_or_clone_tag).and_raise 'Git error'
+ to receive(:checkout_or_clone_version).and_raise 'Git error'
expect { run_rake_task('gitlab:workhorse:install', clone_path) }.to raise_error 'Git error'
end
@@ -32,9 +32,9 @@ describe 'gitlab:workhorse namespace rake task' do
expect(Dir).to receive(:chdir).with(clone_path)
end
- it 'calls checkout_or_clone_tag with the right arguments' do
+ it 'calls checkout_or_clone_version with the right arguments' do
expect_any_instance_of(Object).
- to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path)
+ to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path)
run_rake_task('gitlab:workhorse:install', clone_path)
end
@@ -48,7 +48,7 @@ describe 'gitlab:workhorse namespace rake task' do
context 'gmake is available' do
before do
- expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
+ expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
end
@@ -62,7 +62,7 @@ describe 'gitlab:workhorse namespace rake task' do
context 'gmake is not available' do
before do
- expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
+ expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
end
diff --git a/spec/views/layouts/nav/_project.html.haml_spec.rb b/spec/views/layouts/nav/_project.html.haml_spec.rb
new file mode 100644
index 00000000000..fd1637ca91b
--- /dev/null
+++ b/spec/views/layouts/nav/_project.html.haml_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe 'layouts/nav/_project' do
+ describe 'container registry tab' do
+ before do
+ stub_container_registry_config(enabled: true)
+
+ assign(:project, create(:project))
+ allow(view).to receive(:current_ref).and_return('master')
+
+ allow(view).to receive(:can?).and_return(true)
+ allow(controller).to receive(:controller_name)
+ .and_return('repositories')
+ allow(controller).to receive(:controller_path)
+ .and_return('projects/registry/repositories')
+ end
+
+ it 'has both Registry and Repository tabs' do
+ render
+
+ expect(rendered).to have_text 'Repository'
+ expect(rendered).to have_text 'Registry'
+ end
+
+ it 'highlights only one tab' do
+ render
+
+ expect(rendered).to have_css('.active', count: 1)
+ end
+
+ it 'highlights container registry tab only' do
+ render
+
+ expect(rendered).to have_css('.active', text: 'Registry')
+ end
+ end
+end
diff --git a/spec/views/projects/registry/repositories/index.html.haml_spec.rb b/spec/views/projects/registry/repositories/index.html.haml_spec.rb
new file mode 100644
index 00000000000..ceeace3dc8d
--- /dev/null
+++ b/spec/views/projects/registry/repositories/index.html.haml_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'projects/registry/repositories/index', :view do
+ let(:group) { create(:group, path: 'group') }
+ let(:project) { create(:empty_project, group: group, path: 'test') }
+
+ let(:repository) do
+ create(:container_repository, project: project, name: 'image')
+ end
+
+ before do
+ stub_container_registry_config(enabled: true,
+ host_port: 'registry.gitlab',
+ api_url: 'http://registry.gitlab')
+
+ stub_container_registry_tags(repository: :any, tags: [:latest])
+
+ assign(:project, project)
+ assign(:images, [repository])
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ it 'contains container repository path' do
+ render
+
+ expect(rendered).to have_content 'group/test/image'
+ end
+
+ it 'contains attribute for copying tag location into clipboard' do
+ render
+
+ expect(rendered).to have_css 'button[data-clipboard-text="docker pull ' \
+ 'registry.gitlab/group/test/image:latest"]'
+ end
+end
diff --git a/spec/workers/gitlab_usage_ping_worker_spec.rb b/spec/workers/gitlab_usage_ping_worker_spec.rb
new file mode 100644
index 00000000000..b6c080f36f4
--- /dev/null
+++ b/spec/workers/gitlab_usage_ping_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe GitlabUsagePingWorker do
+ subject { GitlabUsagePingWorker.new }
+
+ it "sends POST request" do
+ stub_application_setting(usage_ping_enabled: true)
+
+ stub_request(:post, "https://version.gitlab.com/usage_data").
+ to_return(status: 200, body: '', headers: {})
+ expect(Gitlab::UsageData).to receive(:to_json).with({ force_refresh: true }).and_call_original
+ expect(subject).to receive(:try_obtain_lease).and_return(true)
+
+ expect(subject.perform.response.code.to_i).to eq(200)
+ end
+
+ it "does not run if usage ping is disabled" do
+ stub_application_setting(usage_ping_enabled: false)
+
+ expect(subject).not_to receive(:try_obtain_lease)
+ expect(subject).not_to receive(:perform)
+ end
+end
diff --git a/spec/workers/schedule_update_user_activity_worker_spec.rb b/spec/workers/schedule_update_user_activity_worker_spec.rb
new file mode 100644
index 00000000000..e583c3203aa
--- /dev/null
+++ b/spec/workers/schedule_update_user_activity_worker_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe ScheduleUpdateUserActivityWorker, :redis do
+ let(:now) { Time.now }
+
+ before do
+ Gitlab::UserActivities.record('1', now)
+ Gitlab::UserActivities.record('2', now)
+ end
+
+ it 'schedules UpdateUserActivityWorker once' do
+ expect(UpdateUserActivityWorker).to receive(:perform_async).with({ '1' => now.to_i.to_s, '2' => now.to_i.to_s })
+
+ subject.perform
+ end
+
+ context 'when specifying a batch size' do
+ it 'schedules UpdateUserActivityWorker twice' do
+ expect(UpdateUserActivityWorker).to receive(:perform_async).with({ '1' => now.to_i.to_s })
+ expect(UpdateUserActivityWorker).to receive(:perform_async).with({ '2' => now.to_i.to_s })
+
+ subject.perform(1)
+ end
+ end
+end
diff --git a/spec/workers/update_user_activity_worker_spec.rb b/spec/workers/update_user_activity_worker_spec.rb
new file mode 100644
index 00000000000..43e9511f116
--- /dev/null
+++ b/spec/workers/update_user_activity_worker_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe UpdateUserActivityWorker, :redis do
+ let(:user_active_2_days_ago) { create(:user, current_sign_in_at: 10.months.ago) }
+ let(:user_active_yesterday_1) { create(:user) }
+ let(:user_active_yesterday_2) { create(:user) }
+ let(:user_active_today) { create(:user) }
+ let(:data) do
+ {
+ user_active_2_days_ago.id.to_s => 2.days.ago.at_midday.to_i.to_s,
+ user_active_yesterday_1.id.to_s => 1.day.ago.at_midday.to_i.to_s,
+ user_active_yesterday_2.id.to_s => 1.day.ago.at_midday.to_i.to_s,
+ user_active_today.id.to_s => Time.now.to_i.to_s
+ }
+ end
+
+ it 'updates users.last_activity_on' do
+ subject.perform(data)
+
+ aggregate_failures do
+ expect(user_active_2_days_ago.reload.last_activity_on).to eq(2.days.ago.to_date)
+ expect(user_active_yesterday_1.reload.last_activity_on).to eq(1.day.ago.to_date)
+ expect(user_active_yesterday_2.reload.last_activity_on).to eq(1.day.ago.to_date)
+ expect(user_active_today.reload.reload.last_activity_on).to eq(Date.today)
+ end
+ end
+
+ it 'deletes the pairs from Redis' do
+ data.each { |id, time| Gitlab::UserActivities.record(id, time) }
+
+ subject.perform(data)
+
+ expect(Gitlab::UserActivities.new.to_a).to be_empty
+ end
+end
diff --git a/vendor/assets/javascripts/notebooklab.js b/vendor/assets/javascripts/notebooklab.js
index 296271205d1..b8cfdc53b48 100644
--- a/vendor/assets/javascripts/notebooklab.js
+++ b/vendor/assets/javascripts/notebooklab.js
@@ -233,22 +233,6 @@ var Component = __webpack_require__(0)(
/* cssModules */
null
)
-Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/prompt.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] prompt.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
- var hotAPI = require("vue-hot-reload-api")
- hotAPI.install(require("vue"), false)
- if (!hotAPI.compatible) return
- module.hot.accept()
- if (!module.hot.data) {
- hotAPI.createRecord("data-v-4f6bf458", Component.options)
- } else {
- hotAPI.reload("data-v-4f6bf458", Component.options)
- }
-})()}
module.exports = Component.exports
@@ -515,22 +499,6 @@ var Component = __webpack_require__(0)(
/* cssModules */
null
)
-Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/code/index.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
- var hotAPI = require("vue-hot-reload-api")
- hotAPI.install(require("vue"), false)
- if (!hotAPI.compatible) return
- module.hot.accept()
- if (!module.hot.data) {
- hotAPI.createRecord("data-v-d42105b8", Component.options)
- } else {
- hotAPI.reload("data-v-d42105b8", Component.options)
- }
-})()}
module.exports = Component.exports
@@ -553,22 +521,6 @@ var Component = __webpack_require__(0)(
/* cssModules */
null
)
-Component.options.__file = "/Users/phil/Projects/notebooklab/src/index.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
- var hotAPI = require("vue-hot-reload-api")
- hotAPI.install(require("vue"), false)
- if (!hotAPI.compatible) return
- module.hot.accept()
- if (!module.hot.data) {
- hotAPI.createRecord("data-v-4cb2b168", Component.options)
- } else {
- hotAPI.reload("data-v-4cb2b168", Component.options)
- }
-})()}
module.exports = Component.exports
@@ -630,9 +582,9 @@ exports.default = {
rawInputCode: function rawInputCode() {
if (this.cell.source) {
return this.cell.source.join('');
- } else {
- return '';
}
+
+ return '';
},
hasOutput: function hasOutput() {
return this.cell.outputs.length;
@@ -747,6 +699,48 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
//
//
+var renderer = new _marked2.default.Renderer();
+
+/*
+ Regex to match KaTex blocks.
+
+ Supports the following:
+
+ \begin{equation}<math>\end{equation}
+ $$<math>$$
+ inline $<math>$
+
+ The matched text then goes through the KaTex renderer & then outputs the HTML
+*/
+var katexRegexString = '(\n ^\\\\begin{[a-zA-Z]+}\\s\n |\n ^\\$\\$\n |\n \\s\\$(?!\\$)\n)\n (.+?)\n(\n \\s\\\\end{[a-zA-Z]+}$\n |\n \\$\\$$\n |\n \\$\n)\n'.replace(/\s/g, '').trim();
+
+renderer.paragraph = function (t) {
+ var text = t;
+ var inline = false;
+
+ if (typeof katex !== 'undefined') {
+ var katexString = text.replace(/\\/g, '\\');
+ var matches = new RegExp(katexRegexString, 'gi').exec(katexString);
+
+ if (matches && matches.length > 0) {
+ if (matches[1].trim() === '$' && matches[3].trim() === '$') {
+ inline = true;
+
+ text = katexString.replace(matches[0], '') + ' ' + katex.renderToString(matches[2]);
+ } else {
+ text = katex.renderToString(matches[2]);
+ }
+ }
+ }
+
+ return '<p class="' + (inline ? 'inline-katex' : '') + '">' + text + '</p>';
+};
+
+_marked2.default.setOptions({
+ sanitize: true,
+ renderer: renderer
+});
+
exports.default = {
components: {
prompt: _prompt2.default
@@ -759,20 +753,7 @@ exports.default = {
},
computed: {
markdown: function markdown() {
- var regex = new RegExp('^\\$\\$(.*)\\$\\$$', 'g');
-
- var source = this.cell.source.map(function (line) {
- var matches = regex.exec(line.trim());
-
- // Only render use the Katex library if it is actually loaded
- if (matches && matches.length > 0 && typeof katex !== 'undefined') {
- return katex.renderToString(matches[1]);
- }
-
- return line;
- });
-
- return (0, _marked2.default)(source.join(''));
+ return (0, _marked2.default)(this.cell.source.join(''));
}
}
};
@@ -1030,13 +1011,14 @@ exports.default = {
cells: []
};
- return this.notebook.worksheets.reduce(function (data, sheet) {
- data.cells = data.cells.concat(sheet.cells);
- return data;
+ return this.notebook.worksheets.reduce(function (cellData, sheet) {
+ var cellDataCopy = cellData;
+ cellDataCopy.cells = cellDataCopy.cells.concat(sheet.cells);
+ return cellDataCopy;
}, data).cells;
- } else {
- return this.notebook.cells;
}
+
+ return this.notebook.cells;
},
hasNotebook: function hasNotebook() {
return Object.keys(this.notebook).length;
@@ -3052,7 +3034,7 @@ exports = module.exports = __webpack_require__(1)(undefined);
// module
-exports.push([module.i, "\n.cell[data-v-3ac4c361] {\n flex-direction: column;\n}\n", ""]);
+exports.push([module.i, ".cell[data-v-3ac4c361]{flex-direction:column}", ""]);
// exports
@@ -3066,7 +3048,7 @@ exports = module.exports = __webpack_require__(1)(undefined);
// module
-exports.push([module.i, "\n.cell,\n.input,\n.output {\n display: flex;\n width: 100%;\n margin-bottom: 10px;\n}\n.cell pre {\n margin: 0;\n width: 100%;\n}\n", ""]);
+exports.push([module.i, ".cell,.input,.output{display:flex;width:100%;margin-bottom:10px}.cell pre{margin:0;width:100%}", ""]);
// exports
@@ -3080,7 +3062,7 @@ exports = module.exports = __webpack_require__(1)(undefined);
// module
-exports.push([module.i, "\n.prompt[data-v-4f6bf458] {\n padding: 0 10px;\n min-width: 7em;\n font-family: monospace;\n}\n", ""]);
+exports.push([module.i, ".prompt[data-v-4f6bf458]{padding:0 10px;min-width:7em;font-family:monospace}", ""]);
// exports
@@ -3094,7 +3076,7 @@ exports = module.exports = __webpack_require__(1)(undefined);
// module
-exports.push([module.i, "\n.markdown .katex {\n display: block;\n text-align: center;\n}\n", ""]);
+exports.push([module.i, ".markdown .katex{display:block;text-align:center}.markdown .inline-katex .katex{display:inline;text-align:initial}", ""]);
// exports
@@ -5382,22 +5364,6 @@ var Component = __webpack_require__(0)(
/* cssModules */
null
)
-Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/code.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] code.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
- var hotAPI = require("vue-hot-reload-api")
- hotAPI.install(require("vue"), false)
- if (!hotAPI.compatible) return
- module.hot.accept()
- if (!module.hot.data) {
- hotAPI.createRecord("data-v-3ac4c361", Component.options)
- } else {
- hotAPI.reload("data-v-3ac4c361", Component.options)
- }
-})()}
module.exports = Component.exports
@@ -5420,22 +5386,6 @@ var Component = __webpack_require__(0)(
/* cssModules */
null
)
-Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/markdown.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] markdown.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
- var hotAPI = require("vue-hot-reload-api")
- hotAPI.install(require("vue"), false)
- if (!hotAPI.compatible) return
- module.hot.accept()
- if (!module.hot.data) {
- hotAPI.createRecord("data-v-7342b363", Component.options)
- } else {
- hotAPI.reload("data-v-7342b363", Component.options)
- }
-})()}
module.exports = Component.exports
@@ -5454,22 +5404,6 @@ var Component = __webpack_require__(0)(
/* cssModules */
null
)
-Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/output/html.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] html.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
- var hotAPI = require("vue-hot-reload-api")
- hotAPI.install(require("vue"), false)
- if (!hotAPI.compatible) return
- module.hot.accept()
- if (!module.hot.data) {
- hotAPI.createRecord("data-v-48ada535", Component.options)
- } else {
- hotAPI.reload("data-v-48ada535", Component.options)
- }
-})()}
module.exports = Component.exports
@@ -5488,22 +5422,6 @@ var Component = __webpack_require__(0)(
/* cssModules */
null
)
-Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/output/image.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] image.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
- var hotAPI = require("vue-hot-reload-api")
- hotAPI.install(require("vue"), false)
- if (!hotAPI.compatible) return
- module.hot.accept()
- if (!module.hot.data) {
- hotAPI.createRecord("data-v-09b68c41", Component.options)
- } else {
- hotAPI.reload("data-v-09b68c41", Component.options)
- }
-})()}
module.exports = Component.exports
@@ -5522,29 +5440,13 @@ var Component = __webpack_require__(0)(
/* cssModules */
null
)
-Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/output/index.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
- var hotAPI = require("vue-hot-reload-api")
- hotAPI.install(require("vue"), false)
- if (!hotAPI.compatible) return
- module.hot.accept()
- if (!module.hot.data) {
- hotAPI.createRecord("data-v-0dec7838", Component.options)
- } else {
- hotAPI.reload("data-v-0dec7838", Component.options)
- }
-})()}
module.exports = Component.exports
/***/ }),
/* 34 */
-/***/ (function(module, exports, __webpack_require__) {
+/***/ (function(module, exports) {
module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
return _c('div', {
@@ -5555,17 +5457,10 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
}
})], 1)
},staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
- module.hot.accept()
- if (module.hot.data) {
- require("vue-hot-reload-api").rerender("data-v-09b68c41", module.exports)
- }
-}
/***/ }),
/* 35 */
-/***/ (function(module, exports, __webpack_require__) {
+/***/ (function(module, exports) {
module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
return _c(_vm.componentName, {
@@ -5579,17 +5474,10 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
}
})
},staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
- module.hot.accept()
- if (module.hot.data) {
- require("vue-hot-reload-api").rerender("data-v-0dec7838", module.exports)
- }
-}
/***/ }),
/* 36 */
-/***/ (function(module, exports, __webpack_require__) {
+/***/ (function(module, exports) {
module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
return _c('div', {
@@ -5609,17 +5497,10 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
}
}) : _vm._e()], 1)
},staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
- module.hot.accept()
- if (module.hot.data) {
- require("vue-hot-reload-api").rerender("data-v-3ac4c361", module.exports)
- }
-}
/***/ }),
/* 37 */
-/***/ (function(module, exports, __webpack_require__) {
+/***/ (function(module, exports) {
module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
return _c('div', {
@@ -5630,17 +5511,10 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
}
})], 1)
},staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
- module.hot.accept()
- if (module.hot.data) {
- require("vue-hot-reload-api").rerender("data-v-48ada535", module.exports)
- }
-}
/***/ }),
/* 38 */
-/***/ (function(module, exports, __webpack_require__) {
+/***/ (function(module, exports) {
module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
return (_vm.hasNotebook) ? _c('div', _vm._l((_vm.cells), function(cell, index) {
@@ -5654,34 +5528,20 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
})
})) : _vm._e()
},staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
- module.hot.accept()
- if (module.hot.data) {
- require("vue-hot-reload-api").rerender("data-v-4cb2b168", module.exports)
- }
-}
/***/ }),
/* 39 */
-/***/ (function(module, exports, __webpack_require__) {
+/***/ (function(module, exports) {
module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
return _c('div', {
staticClass: "prompt"
}, [(_vm.type && _vm.count) ? _c('span', [_vm._v("\n " + _vm._s(_vm.type) + " [" + _vm._s(_vm.count) + "]:\n ")]) : _vm._e()])
},staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
- module.hot.accept()
- if (module.hot.data) {
- require("vue-hot-reload-api").rerender("data-v-4f6bf458", module.exports)
- }
-}
/***/ }),
/* 40 */
-/***/ (function(module, exports, __webpack_require__) {
+/***/ (function(module, exports) {
module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
return _c('div', {
@@ -5693,17 +5553,10 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
}
})], 1)
},staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
- module.hot.accept()
- if (module.hot.data) {
- require("vue-hot-reload-api").rerender("data-v-7342b363", module.exports)
- }
-}
/***/ }),
/* 41 */
-/***/ (function(module, exports, __webpack_require__) {
+/***/ (function(module, exports) {
module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
return _c('div', {
@@ -5722,13 +5575,6 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
}
}, [_vm._v("\n ")])], 1)
},staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
- module.hot.accept()
- if (module.hot.data) {
- require("vue-hot-reload-api").rerender("data-v-d42105b8", module.exports)
- }
-}
/***/ }),
/* 42 */
@@ -5741,13 +5587,13 @@ var content = __webpack_require__(19);
if(typeof content === 'string') content = [[module.i, content, '']];
if(content.locals) module.exports = content.locals;
// add the styles to the DOM
-var update = __webpack_require__(3)("06fc6a9f", content, false);
+var update = __webpack_require__(3)("74a276de", content, true);
// Hot Module Replacement
if(false) {
// When the styles change, update the <style> tags
if(!content.locals) {
- module.hot.accept("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-3ac4c361\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./code.vue", function() {
- var newContent = require("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-3ac4c361\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./code.vue");
+ module.hot.accept("!!../../node_modules/css-loader/index.js?minimize!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-3ac4c361\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./code.vue", function() {
+ var newContent = require("!!../../node_modules/css-loader/index.js?minimize!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-3ac4c361\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./code.vue");
if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
update(newContent);
});
@@ -5767,13 +5613,13 @@ var content = __webpack_require__(20);
if(typeof content === 'string') content = [[module.i, content, '']];
if(content.locals) module.exports = content.locals;
// add the styles to the DOM
-var update = __webpack_require__(3)("87c28124", content, false);
+var update = __webpack_require__(3)("55f9d67b", content, true);
// Hot Module Replacement
if(false) {
// When the styles change, update the <style> tags
if(!content.locals) {
- module.hot.accept("!!../node_modules/css-loader/index.js!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4cb2b168\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue", function() {
- var newContent = require("!!../node_modules/css-loader/index.js!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4cb2b168\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue");
+ module.hot.accept("!!../node_modules/css-loader/index.js?minimize!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4cb2b168\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue", function() {
+ var newContent = require("!!../node_modules/css-loader/index.js?minimize!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4cb2b168\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue");
if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
update(newContent);
});
@@ -5793,13 +5639,13 @@ var content = __webpack_require__(21);
if(typeof content === 'string') content = [[module.i, content, '']];
if(content.locals) module.exports = content.locals;
// add the styles to the DOM
-var update = __webpack_require__(3)("5b60b003", content, false);
+var update = __webpack_require__(3)("1096aefc", content, true);
// Hot Module Replacement
if(false) {
// When the styles change, update the <style> tags
if(!content.locals) {
- module.hot.accept("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4f6bf458\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./prompt.vue", function() {
- var newContent = require("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4f6bf458\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./prompt.vue");
+ module.hot.accept("!!../../node_modules/css-loader/index.js?minimize!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4f6bf458\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./prompt.vue", function() {
+ var newContent = require("!!../../node_modules/css-loader/index.js?minimize!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4f6bf458\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./prompt.vue");
if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
update(newContent);
});
@@ -5819,13 +5665,13 @@ var content = __webpack_require__(22);
if(typeof content === 'string') content = [[module.i, content, '']];
if(content.locals) module.exports = content.locals;
// add the styles to the DOM
-var update = __webpack_require__(3)("48dda57c", content, false);
+var update = __webpack_require__(3)("58a0689d", content, true);
// Hot Module Replacement
if(false) {
// When the styles change, update the <style> tags
if(!content.locals) {
- module.hot.accept("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7342b363\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./markdown.vue", function() {
- var newContent = require("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7342b363\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./markdown.vue");
+ module.hot.accept("!!../../node_modules/css-loader/index.js?minimize!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7342b363\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./markdown.vue", function() {
+ var newContent = require("!!../../node_modules/css-loader/index.js?minimize!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7342b363\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./markdown.vue");
if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
update(newContent);
});
diff --git a/vendor/assets/javascripts/pdf.worker.js b/vendor/assets/javascripts/pdf.worker.js
index f8a94e207f8..970caaaba86 100644
--- a/vendor/assets/javascripts/pdf.worker.js
+++ b/vendor/assets/javascripts/pdf.worker.js
@@ -73,7 +73,7 @@ return /******/ (function(modules) { // webpackBootstrap
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
-/******/ return __webpack_require__(__webpack_require__.s = 16);
+/******/ return __webpack_require__(__webpack_require__.s = 24);
/******/ })
/************************************************************************/
/******/ ({
@@ -20214,6 +20214,7 @@ var stringToUTF8String = sharedUtil.stringToUTF8String;
var warn = sharedUtil.warn;
var createValidAbsoluteUrl = sharedUtil.createValidAbsoluteUrl;
var Util = sharedUtil.Util;
+var Dict = corePrimitives.Dict;
var Ref = corePrimitives.Ref;
var RefSet = corePrimitives.RefSet;
var RefSetCache = corePrimitives.RefSetCache;
@@ -20233,9 +20234,10 @@ var Catalog = function CatalogClosure() {
this.pdfManager = pdfManager;
this.xref = xref;
this.catDict = xref.getCatalogObj();
+ assert(isDict(this.catDict), 'catalog object is not a dictionary');
this.fontCache = new RefSetCache();
this.builtInCMapCache = Object.create(null);
- assert(isDict(this.catDict), 'catalog object is not a dictionary');
+ this.pageKidsCountCache = new RefSetCache();
this.pageFactory = pageFactory;
this.pagePromises = [];
}
@@ -20551,6 +20553,7 @@ var Catalog = function CatalogClosure() {
return shadow(this, 'javaScript', javaScript);
},
cleanup: function Catalog_cleanup() {
+ this.pageKidsCountCache.clear();
var promises = [];
this.fontCache.forEach(function (promise) {
promises.push(promise);
@@ -20577,15 +20580,25 @@ var Catalog = function CatalogClosure() {
getPageDict: function Catalog_getPageDict(pageIndex) {
var capability = createPromiseCapability();
var nodesToVisit = [this.catDict.getRaw('Pages')];
- var currentPageIndex = 0;
- var xref = this.xref;
+ var count,
+ currentPageIndex = 0;
+ var xref = this.xref,
+ pageKidsCountCache = this.pageKidsCountCache;
function next() {
while (nodesToVisit.length) {
var currentNode = nodesToVisit.pop();
if (isRef(currentNode)) {
+ count = pageKidsCountCache.get(currentNode);
+ if (count > 0 && currentPageIndex + count < pageIndex) {
+ currentPageIndex += count;
+ continue;
+ }
xref.fetchAsync(currentNode).then(function (obj) {
if (isDict(obj, 'Page') || isDict(obj) && !obj.has('Kids')) {
if (pageIndex === currentPageIndex) {
+ if (currentNode && !pageKidsCountCache.has(currentNode)) {
+ pageKidsCountCache.put(currentNode, 1);
+ }
capability.resolve([obj, currentNode]);
} else {
currentPageIndex++;
@@ -20599,7 +20612,11 @@ var Catalog = function CatalogClosure() {
return;
}
assert(isDict(currentNode), 'page dictionary kid reference points to wrong type of object');
- var count = currentNode.get('Count');
+ count = currentNode.get('Count');
+ var objId = currentNode.objId;
+ if (objId && !pageKidsCountCache.has(objId)) {
+ pageKidsCountCache.put(objId, count);
+ }
if (currentPageIndex + count <= pageIndex) {
currentPageIndex += count;
continue;
@@ -21191,7 +21208,7 @@ var XRef = function XRefClosure() {
var num = ref.num;
if (num in this.cache) {
var cacheEntry = this.cache[num];
- if (isDict(cacheEntry) && !cacheEntry.objId) {
+ if (cacheEntry instanceof Dict && !cacheEntry.objId) {
cacheEntry.objId = ref.toString();
}
return cacheEntry;
@@ -26178,7 +26195,7 @@ var CMapFactory = function CMapFactoryClosure() {
return Promise.resolve(new IdentityCMap(true, 2));
}
if (BUILT_IN_CMAPS.indexOf(name) === -1) {
- return Promise.reject(new Error('Unknown cMap name: ' + name));
+ return Promise.reject(new Error('Unknown CMap name: ' + name));
}
assert(fetchBuiltInCMap, 'Built-in CMap parameters are not provided.');
return fetchBuiltInCMap(name).then(function (data) {
@@ -28458,9 +28475,6 @@ var Font = function FontClosure() {
}
glyphId = offsetIndex < 0 ? j : offsets[offsetIndex + j - start];
glyphId = glyphId + delta & 0xFFFF;
- if (glyphId === 0) {
- continue;
- }
mappings.push({
charCode: j,
glyphId: glyphId
@@ -37160,8 +37174,8 @@ exports.Type1Parser = Type1Parser;
"use strict";
-var pdfjsVersion = '1.7.395';
-var pdfjsBuild = '07f7c97b';
+var pdfjsVersion = '1.8.172';
+var pdfjsBuild = '8ff1fbe7';
var pdfjsCoreWorker = __w_pdfjs_require__(8);
{
__w_pdfjs_require__(19);
@@ -37646,20 +37660,28 @@ if (typeof PDFJS === 'undefined' || !PDFJS.compatibilityChecked) {
}
})();
(function checkRequestAnimationFrame() {
- function fakeRequestAnimationFrame(callback) {
- window.setTimeout(callback, 20);
+ function installFakeAnimationFrameFunctions() {
+ window.requestAnimationFrame = function (callback) {
+ return window.setTimeout(callback, 20);
+ };
+ window.cancelAnimationFrame = function (timeoutID) {
+ window.clearTimeout(timeoutID);
+ };
}
if (!hasDOM) {
return;
}
if (isIOS) {
- window.requestAnimationFrame = fakeRequestAnimationFrame;
+ installFakeAnimationFrameFunctions();
return;
}
if ('requestAnimationFrame' in window) {
return;
}
- window.requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || fakeRequestAnimationFrame;
+ window.requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame;
+ if (!('requestAnimationFrame' in window)) {
+ installFakeAnimationFrameFunctions();
+ }
})();
(function checkCanvasSizeLimitation() {
if (isIOS || isAndroid) {
@@ -38588,7 +38610,7 @@ if (typeof PDFJS === 'undefined' || !PDFJS.compatibilityChecked) {
/***/ }),
-/***/ 16:
+/***/ 24:
/***/ (function(module, exports, __webpack_require__) {
/* Copyright 2016 Mozilla Foundation
diff --git a/vendor/assets/javascripts/pdflab.js b/vendor/assets/javascripts/pdflab.js
index 94e7c40e75e..5d9c348ce35 100644
--- a/vendor/assets/javascripts/pdflab.js
+++ b/vendor/assets/javascripts/pdflab.js
@@ -71,17 +71,10 @@ return /******/ (function(modules) { // webpackBootstrap
/******/ if(installedChunks[chunkId] === 0)
/******/ return Promise.resolve();
/******/
-/******/ // a Promise means "currently loading".
+/******/ // an Promise means "currently loading".
/******/ if(installedChunks[chunkId]) {
/******/ return installedChunks[chunkId][2];
/******/ }
-/******/
-/******/ // setup Promise in chunk cache
-/******/ var promise = new Promise(function(resolve, reject) {
-/******/ installedChunks[chunkId] = [resolve, reject];
-/******/ });
-/******/ installedChunks[chunkId][2] = promise;
-/******/
/******/ // start chunk loading
/******/ var head = document.getElementsByTagName('head')[0];
/******/ var script = document.createElement('script');
@@ -106,8 +99,13 @@ return /******/ (function(modules) { // webpackBootstrap
/******/ installedChunks[chunkId] = undefined;
/******/ }
/******/ };
-/******/ head.appendChild(script);
/******/
+/******/ var promise = new Promise(function(resolve, reject) {
+/******/ installedChunks[chunkId] = [resolve, reject];
+/******/ });
+/******/ installedChunks[chunkId][2] = promise;
+/******/
+/******/ head.appendChild(script);
/******/ return promise;
/******/ };
/******/
@@ -150,7 +148,7 @@ return /******/ (function(modules) { // webpackBootstrap
/******/ __webpack_require__.oe = function(err) { console.error(err); throw err; };
/******/
/******/ // Load entry module and return exports
-/******/ return __webpack_require__(__webpack_require__.s = 7);
+/******/ return __webpack_require__(__webpack_require__.s = 23);
/******/ })
/************************************************************************/
/******/ ([
@@ -1615,7 +1613,10 @@ var DOMCMapReaderFactory = function DOMCMapReaderFactoryClosure() {
request.responseType = 'arraybuffer';
}
request.onreadystatechange = function () {
- if (request.readyState === XMLHttpRequest.DONE && (request.status === 200 || request.status === 0)) {
+ if (request.readyState !== XMLHttpRequest.DONE) {
+ return;
+ }
+ if (request.status === 200 || request.status === 0) {
var data;
if (this.isCompressed && request.response) {
data = new Uint8Array(request.response);
@@ -1629,8 +1630,8 @@ var DOMCMapReaderFactory = function DOMCMapReaderFactoryClosure() {
});
return;
}
- reject(new Error('Unable to load ' + (this.isCompressed ? 'binary ' : '') + 'CMap at: ' + url));
}
+ reject(new Error('Unable to load ' + (this.isCompressed ? 'binary ' : '') + 'CMap at: ' + url));
}.bind(this);
request.send(null);
}.bind(this));
@@ -1670,6 +1671,16 @@ var CustomStyle = function CustomStyleClosure() {
};
return CustomStyle;
}();
+var RenderingCancelledException = function RenderingCancelledException() {
+ function RenderingCancelledException(msg, type) {
+ this.message = msg;
+ this.type = type;
+ }
+ RenderingCancelledException.prototype = new Error();
+ RenderingCancelledException.prototype.name = 'RenderingCancelledException';
+ RenderingCancelledException.constructor = RenderingCancelledException;
+ return RenderingCancelledException;
+}();
var hasCanvasTypedArrays;
hasCanvasTypedArrays = function hasCanvasTypedArrays() {
var canvas = document.createElement('canvas');
@@ -1762,6 +1773,8 @@ function getDefaultSetting(id) {
return globalSettings ? globalSettings.externalLinkRel : DEFAULT_LINK_REL;
case 'enableStats':
return !!(globalSettings && globalSettings.enableStats);
+ case 'pdfjsNext':
+ return !!(globalSettings && globalSettings.pdfjsNext);
default:
throw new Error('Unknown default setting: ' + id);
}
@@ -1789,6 +1802,7 @@ exports.isExternalLinkTargetSet = isExternalLinkTargetSet;
exports.isValidUrl = isValidUrl;
exports.getFilenameFromUrl = getFilenameFromUrl;
exports.LinkTarget = LinkTarget;
+exports.RenderingCancelledException = RenderingCancelledException;
exports.hasCanvasTypedArrays = hasCanvasTypedArrays;
exports.getDefaultSetting = getDefaultSetting;
exports.DEFAULT_LINK_REL = DEFAULT_LINK_REL;
@@ -2450,6 +2464,7 @@ var FontFaceObject = displayFontLoader.FontFaceObject;
var FontLoader = displayFontLoader.FontLoader;
var CanvasGraphics = displayCanvas.CanvasGraphics;
var Metadata = displayMetadata.Metadata;
+var RenderingCancelledException = displayDOMUtils.RenderingCancelledException;
var getDefaultSetting = displayDOMUtils.getDefaultSetting;
var DOMCanvasFactory = displayDOMUtils.DOMCanvasFactory;
var DOMCMapReaderFactory = displayDOMUtils.DOMCMapReaderFactory;
@@ -3711,7 +3726,11 @@ var InternalRenderTask = function InternalRenderTaskClosure() {
cancel: function InternalRenderTask_cancel() {
this.running = false;
this.cancelled = true;
- this.callback('cancelled');
+ if (getDefaultSetting('pdfjsNext')) {
+ this.callback(new RenderingCancelledException('Rendering cancelled, page ' + this.pageNumber, 'canvas'));
+ } else {
+ this.callback('cancelled');
+ }
},
operatorListChanged: function InternalRenderTask_operatorListChanged() {
if (!this.graphicsReady) {
@@ -3776,8 +3795,8 @@ var _UnsupportedManager = function UnsupportedManagerClosure() {
}
};
}();
-exports.version = '1.7.395';
-exports.build = '07f7c97b';
+exports.version = '1.8.172';
+exports.build = '8ff1fbe7';
exports.getDocument = getDocument;
exports.PDFDataRangeTransport = PDFDataRangeTransport;
exports.PDFWorker = PDFWorker;
@@ -5716,8 +5735,8 @@ if (!globalScope.PDFJS) {
globalScope.PDFJS = {};
}
var PDFJS = globalScope.PDFJS;
-PDFJS.version = '1.7.395';
-PDFJS.build = '07f7c97b';
+PDFJS.version = '1.8.172';
+PDFJS.build = '8ff1fbe7';
PDFJS.pdfBug = false;
if (PDFJS.verbosity !== undefined) {
sharedUtil.setVerbosityLevel(PDFJS.verbosity);
@@ -5777,6 +5796,7 @@ PDFJS.disableWebGL = PDFJS.disableWebGL === undefined ? true : PDFJS.disableWebG
PDFJS.externalLinkTarget = PDFJS.externalLinkTarget === undefined ? LinkTarget.NONE : PDFJS.externalLinkTarget;
PDFJS.externalLinkRel = PDFJS.externalLinkRel === undefined ? DEFAULT_LINK_REL : PDFJS.externalLinkRel;
PDFJS.isEvalSupported = PDFJS.isEvalSupported === undefined ? true : PDFJS.isEvalSupported;
+PDFJS.pdfjsNext = PDFJS.pdfjsNext === undefined ? false : PDFJS.pdfjsNext;
var savedOpenExternalLinksInNewWindow = PDFJS.openExternalLinksInNewWindow;
delete PDFJS.openExternalLinksInNewWindow;
Object.defineProperty(PDFJS, 'openExternalLinksInNewWindow', {
@@ -8227,8 +8247,8 @@ exports.TilingPattern = TilingPattern;
"use strict";
-var pdfjsVersion = '1.7.395';
-var pdfjsBuild = '07f7c97b';
+var pdfjsVersion = '1.8.172';
+var pdfjsBuild = '8ff1fbe7';
var pdfjsSharedUtil = __w_pdfjs_require__(0);
var pdfjsDisplayGlobal = __w_pdfjs_require__(9);
var pdfjsDisplayAPI = __w_pdfjs_require__(3);
@@ -8259,6 +8279,7 @@ exports.createObjectURL = pdfjsSharedUtil.createObjectURL;
exports.removeNullCharacters = pdfjsSharedUtil.removeNullCharacters;
exports.shadow = pdfjsSharedUtil.shadow;
exports.createBlob = pdfjsSharedUtil.createBlob;
+exports.RenderingCancelledException = pdfjsDisplayDOMUtils.RenderingCancelledException;
exports.getFilenameFromUrl = pdfjsDisplayDOMUtils.getFilenameFromUrl;
exports.addLinkAttributes = pdfjsDisplayDOMUtils.addLinkAttributes;
@@ -8740,20 +8761,28 @@ if (typeof PDFJS === 'undefined' || !PDFJS.compatibilityChecked) {
}
})();
(function checkRequestAnimationFrame() {
- function fakeRequestAnimationFrame(callback) {
- window.setTimeout(callback, 20);
+ function installFakeAnimationFrameFunctions() {
+ window.requestAnimationFrame = function (callback) {
+ return window.setTimeout(callback, 20);
+ };
+ window.cancelAnimationFrame = function (timeoutID) {
+ window.clearTimeout(timeoutID);
+ };
}
if (!hasDOM) {
return;
}
if (isIOS) {
- window.requestAnimationFrame = fakeRequestAnimationFrame;
+ installFakeAnimationFrameFunctions();
return;
}
if ('requestAnimationFrame' in window) {
return;
}
- window.requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || fakeRequestAnimationFrame;
+ window.requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame;
+ if (!('requestAnimationFrame' in window)) {
+ installFakeAnimationFrameFunctions();
+ }
})();
(function checkCanvasSizeLimitation() {
if (isIOS || isAndroid) {
@@ -9760,7 +9789,7 @@ function toComment(sourceMap) {
return '/*# ' + data + ' */';
}
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(11).Buffer))
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(10).Buffer))
/***/ }),
/* 4 */
@@ -9839,7 +9868,7 @@ if (typeof DEBUG !== 'undefined' && DEBUG) {
) }
}
-var listToStyles = __webpack_require__(23)
+var listToStyles = __webpack_require__(21)
/*
type StyleObject = {
@@ -10046,34 +10075,18 @@ function applyToTag (styleElement, obj) {
/* styles */
-__webpack_require__(21)
+__webpack_require__(19)
var Component = __webpack_require__(4)(
/* script */
- __webpack_require__(8),
+ __webpack_require__(7),
/* template */
- __webpack_require__(19),
+ __webpack_require__(17),
/* scopeId */
null,
/* cssModules */
null
)
-Component.options.__file = "/Users/samrose/Projects/pdflab/src/index.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
- var hotAPI = require("vue-hot-reload-api")
- hotAPI.install(require("vue"), false)
- if (!hotAPI.compatible) return
- module.hot.accept()
- if (!module.hot.data) {
- hotAPI.createRecord("data-v-7c7bed7e", Component.options)
- } else {
- hotAPI.reload("data-v-7c7bed7e", Component.options)
- }
-})()}
module.exports = Component.exports
@@ -10085,25 +10098,6 @@ module.exports = Component.exports
"use strict";
-var PDF = __webpack_require__(6);
-var pdfjsLib = __webpack_require__(2);
-
-module.exports = {
- install: function install(_vue, _ref) {
- var workerSrc = _ref.workerSrc;
-
- pdfjsLib.PDFJS.workerSrc = workerSrc;
- _vue.component('pdf-lab', PDF);
- }
-};
-
-/***/ }),
-/* 8 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
Object.defineProperty(exports, "__esModule", {
value: true
});
@@ -10112,7 +10106,7 @@ var _pdfjsDist = __webpack_require__(2);
var _pdfjsDist2 = _interopRequireDefault(_pdfjsDist);
-var _index = __webpack_require__(18);
+var _index = __webpack_require__(16);
var _index2 = _interopRequireDefault(_index);
@@ -10138,7 +10132,7 @@ exports.default = {
},
data: function data() {
return {
- isLoading: false,
+ loading: false,
pages: []
};
},
@@ -10163,17 +10157,17 @@ exports.default = {
}).catch(function (error) {
return _this.$emit('pdflaberror', error);
}).then(function () {
- return _this.isLoading = false;
+ _this.loading = false;
});
},
renderPages: function renderPages(pdf) {
var _this2 = this;
var pagePromises = [];
- this.isLoading = true;
- for (var num = 1; num <= pdf.numPages; num++) {
- pagePromises.push(pdf.getPage(num).then(function (page) {
- return _this2.pages.push(page);
+ this.loading = true;
+ for (var num = 1; num <= pdf.numPages; num += 1) {
+ pagePromises.push(pdf.getPage(num).then(function (p) {
+ return _this2.pages.push(p);
}));
}
return Promise.all(pagePromises);
@@ -10185,7 +10179,7 @@ exports.default = {
};
/***/ }),
-/* 9 */
+/* 8 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@@ -10213,10 +10207,16 @@ exports.default = {
required: true
}
},
+ data: function data() {
+ return {
+ scale: 4,
+ rendering: false
+ };
+ },
+
computed: {
viewport: function viewport() {
- var scale = 4;
- return this.page.getViewport(scale);
+ return this.page.getViewport(this.scale);
},
context: function context() {
return this.$refs.canvas.getContext('2d');
@@ -10229,14 +10229,19 @@ exports.default = {
}
},
mounted: function mounted() {
+ var _this = this;
+
this.$refs.canvas.height = this.viewport.height;
this.$refs.canvas.width = this.viewport.width;
- this.page.render(this.renderContext);
+ this.rendering = true;
+ this.page.render(this.renderContext).then(function () {
+ _this.rendering = false;
+ });
}
};
/***/ }),
-/* 10 */
+/* 9 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@@ -10357,7 +10362,7 @@ function fromByteArray (uint8) {
/***/ }),
-/* 11 */
+/* 10 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@@ -10371,9 +10376,9 @@ function fromByteArray (uint8) {
-var base64 = __webpack_require__(10)
-var ieee754 = __webpack_require__(14)
-var isArray = __webpack_require__(15)
+var base64 = __webpack_require__(9)
+var ieee754 = __webpack_require__(13)
+var isArray = __webpack_require__(14)
exports.Buffer = Buffer
exports.SlowBuffer = SlowBuffer
@@ -12151,10 +12156,10 @@ function isnan (val) {
return val !== val // eslint-disable-line no-self-compare
}
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(24)))
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(22)))
/***/ }),
-/* 12 */
+/* 11 */
/***/ (function(module, exports, __webpack_require__) {
exports = module.exports = __webpack_require__(3)(undefined);
@@ -12162,13 +12167,13 @@ exports = module.exports = __webpack_require__(3)(undefined);
// module
-exports.push([module.i, "\n.pdf-viewer {\n background: url(" + __webpack_require__(17) + ");\n display: flex;\n flex-flow: column nowrap;\n}\n", ""]);
+exports.push([module.i, ".pdf-viewer{background:url(" + __webpack_require__(15) + ");display:flex;flex-flow:column nowrap}", ""]);
// exports
/***/ }),
-/* 13 */
+/* 12 */
/***/ (function(module, exports, __webpack_require__) {
exports = module.exports = __webpack_require__(3)(undefined);
@@ -12176,13 +12181,13 @@ exports = module.exports = __webpack_require__(3)(undefined);
// module
-exports.push([module.i, "\n.pdf-page {\n margin: 8px auto 0 auto;\n border-top: 1px #ddd solid;\n border-bottom: 1px #ddd solid;\n width: 100%;\n}\n.pdf-page:first-child {\n margin-top: 0px;\n border-top: 0px;\n}\n.pdf-page:last-child {\n margin-bottom: 0px;\n border-bottom: 0px;\n}\n", ""]);
+exports.push([module.i, ".pdf-page{margin:8px auto 0;border-top:1px solid #ddd;border-bottom:1px solid #ddd;width:100%}.pdf-page:first-child{margin-top:0;border-top:0}.pdf-page:last-child{margin-bottom:0;border-bottom:0}", ""]);
// exports
/***/ }),
-/* 14 */
+/* 13 */
/***/ (function(module, exports) {
exports.read = function (buffer, offset, isLE, mLen, nBytes) {
@@ -12272,7 +12277,7 @@ exports.write = function (buffer, value, offset, isLE, mLen, nBytes) {
/***/ }),
-/* 15 */
+/* 14 */
/***/ (function(module, exports) {
var toString = {}.toString;
@@ -12283,53 +12288,36 @@ module.exports = Array.isArray || function (arr) {
/***/ }),
-/* 16 */,
-/* 17 */
+/* 15 */
/***/ (function(module, exports) {
module.exports = "data:image/gif;base64,R0lGODlhCgAKAIAAAOXl5f///yH5BAAAAAAALAAAAAAKAAoAAAIRhB2ZhxoM3GMSykqd1VltzxQAOw=="
/***/ }),
-/* 18 */
+/* 16 */
/***/ (function(module, exports, __webpack_require__) {
/* styles */
-__webpack_require__(22)
+__webpack_require__(20)
var Component = __webpack_require__(4)(
/* script */
- __webpack_require__(9),
+ __webpack_require__(8),
/* template */
- __webpack_require__(20),
+ __webpack_require__(18),
/* scopeId */
null,
/* cssModules */
null
)
-Component.options.__file = "/Users/samrose/Projects/pdflab/src/page/index.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
- var hotAPI = require("vue-hot-reload-api")
- hotAPI.install(require("vue"), false)
- if (!hotAPI.compatible) return
- module.hot.accept()
- if (!module.hot.data) {
- hotAPI.createRecord("data-v-7e912b1a", Component.options)
- } else {
- hotAPI.reload("data-v-7e912b1a", Component.options)
- }
-})()}
module.exports = Component.exports
/***/ }),
-/* 19 */
-/***/ (function(module, exports, __webpack_require__) {
+/* 17 */
+/***/ (function(module, exports) {
module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
return (_vm.hasPDF) ? _c('div', {
@@ -12338,24 +12326,17 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
return _c('page', {
key: index,
attrs: {
- "v-if": !_vm.isLoading,
+ "v-if": !_vm.loading,
"page": page,
"number": index + 1
}
})
})) : _vm._e()
},staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
- module.hot.accept()
- if (module.hot.data) {
- require("vue-hot-reload-api").rerender("data-v-7c7bed7e", module.exports)
- }
-}
/***/ }),
-/* 20 */
-/***/ (function(module, exports, __webpack_require__) {
+/* 18 */
+/***/ (function(module, exports) {
module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
return _c('canvas', {
@@ -12366,32 +12347,25 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
}
})
},staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
- module.hot.accept()
- if (module.hot.data) {
- require("vue-hot-reload-api").rerender("data-v-7e912b1a", module.exports)
- }
-}
/***/ }),
-/* 21 */
+/* 19 */
/***/ (function(module, exports, __webpack_require__) {
// style-loader: Adds some css to the DOM by adding a <style> tag
// load the styles
-var content = __webpack_require__(12);
+var content = __webpack_require__(11);
if(typeof content === 'string') content = [[module.i, content, '']];
if(content.locals) module.exports = content.locals;
// add the styles to the DOM
-var update = __webpack_require__(5)("8018213c", content, false);
+var update = __webpack_require__(5)("59cf066f", content, true);
// Hot Module Replacement
if(false) {
// When the styles change, update the <style> tags
if(!content.locals) {
- module.hot.accept("!!../node_modules/css-loader/index.js!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7c7bed7e\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue", function() {
- var newContent = require("!!../node_modules/css-loader/index.js!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7c7bed7e\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue");
+ module.hot.accept("!!../node_modules/css-loader/index.js?minimize!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7c7bed7e\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue", function() {
+ var newContent = require("!!../node_modules/css-loader/index.js?minimize!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7c7bed7e\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue");
if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
update(newContent);
});
@@ -12401,23 +12375,23 @@ if(false) {
}
/***/ }),
-/* 22 */
+/* 20 */
/***/ (function(module, exports, __webpack_require__) {
// style-loader: Adds some css to the DOM by adding a <style> tag
// load the styles
-var content = __webpack_require__(13);
+var content = __webpack_require__(12);
if(typeof content === 'string') content = [[module.i, content, '']];
if(content.locals) module.exports = content.locals;
// add the styles to the DOM
-var update = __webpack_require__(5)("6d9dea59", content, false);
+var update = __webpack_require__(5)("09f1e2d8", content, true);
// Hot Module Replacement
if(false) {
// When the styles change, update the <style> tags
if(!content.locals) {
- module.hot.accept("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7e912b1a\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue", function() {
- var newContent = require("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7e912b1a\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue");
+ module.hot.accept("!!../../node_modules/css-loader/index.js?minimize!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7e912b1a\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue", function() {
+ var newContent = require("!!../../node_modules/css-loader/index.js?minimize!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7e912b1a\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue");
if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
update(newContent);
});
@@ -12427,7 +12401,7 @@ if(false) {
}
/***/ }),
-/* 23 */
+/* 21 */
/***/ (function(module, exports) {
/**
@@ -12460,7 +12434,7 @@ module.exports = function listToStyles (parentId, list) {
/***/ }),
-/* 24 */
+/* 22 */
/***/ (function(module, exports) {
var g;
@@ -12486,6 +12460,25 @@ try {
module.exports = g;
+/***/ }),
+/* 23 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var PDF = __webpack_require__(6);
+var pdfjsLib = __webpack_require__(2);
+
+module.exports = {
+ install: function install(_vue) {
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ pdfjsLib.PDFJS.workerSrc = options.workerSrc || '';
+ _vue.component('pdf-lab', PDF);
+ }
+};
+
/***/ })
/******/ ]);
}); \ No newline at end of file
diff --git a/vendor/gitignore/C.gitignore b/vendor/gitignore/C.gitignore
index 8a365b3d829..c6127b38c1a 100644
--- a/vendor/gitignore/C.gitignore
+++ b/vendor/gitignore/C.gitignore
@@ -45,6 +45,7 @@
# Kernel Module Compile Results
*.mod*
*.cmd
+.tmp_versions/
modules.order
Module.symvers
Mkfile.old
diff --git a/vendor/gitignore/Dart.gitignore b/vendor/gitignore/Dart.gitignore
index 4b366585ddc..4d2a4d6db7c 100644
--- a/vendor/gitignore/Dart.gitignore
+++ b/vendor/gitignore/Dart.gitignore
@@ -1,33 +1,12 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
-
-# SDK 1.20 and later (no longer creates packages directories)
.packages
.pub/
build/
-
-# Older SDK versions
-# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
-.project
-.buildlog
-**/packages/
-
-
-# Files created by dart2js
-# (Most Dart developers will use pub build to compile Dart, use/modify these
-# rules if you intend to use dart2js directly
-# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
-# differentiate from explicit Javascript files)
-*.dart.js
-*.part.js
-*.js.deps
-*.js.map
-*.info.json
+# If you're building an application, you may want to check-in your pubspec.lock
+pubspec.lock
# Directory created by dartdoc
+# If you don't generate documentation locally you can remove this line.
doc/api/
-
-# Don't commit pubspec lock file
-# (Library packages only! Remove pattern if developing an application package)
-pubspec.lock
diff --git a/vendor/gitignore/Global/Eclipse.gitignore b/vendor/gitignore/Global/Eclipse.gitignore
index 4f88399d2d8..ce1c12cdb7a 100644
--- a/vendor/gitignore/Global/Eclipse.gitignore
+++ b/vendor/gitignore/Global/Eclipse.gitignore
@@ -11,9 +11,6 @@ local.properties
.loadpath
.recommenders
-# Eclipse Core
-.project
-
# External tool builders
.externalToolBuilders/
@@ -26,9 +23,6 @@ local.properties
# CDT-specific (C/C++ Development Tooling)
.cproject
-# JDT-specific (Eclipse Java Development Tools)
-.classpath
-
# Java annotation processor (APT)
.factorypath
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index ec7e95c6ab5..a5d4cc86d33 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -36,6 +36,9 @@
# JIRA plugin
atlassian-ide-plugin.xml
+# Cursive Clojure plugin
+.idea/replstate.xml
+
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
diff --git a/vendor/gitignore/Global/macOS.gitignore b/vendor/gitignore/Global/macOS.gitignore
index f0f3fbc06c8..5972fe50f66 100644
--- a/vendor/gitignore/Global/macOS.gitignore
+++ b/vendor/gitignore/Global/macOS.gitignore
@@ -1,26 +1,25 @@
-*.DS_Store
-.AppleDouble
-.LSOverride
-
-# Icon must end with two \r
-Icon
-
-
-# Thumbnails
-._*
-
-# Files that might appear in the root of a volume
-.DocumentRevisions-V100
-.fseventsd
-.Spotlight-V100
-.TemporaryItems
-.Trashes
-.VolumeIcon.icns
-.com.apple.timemachine.donotpresent
-
-# Directories potentially created on remote AFP share
-.AppleDB
-.AppleDesktop
-Network Trash Folder
-Temporary Items
-.apdisk
+*.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 62c1e736924..ff65a437185 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -92,3 +92,6 @@ ENV/
# Rope project settings
.ropeproject
+
+# mkdocs documentation
+/site
diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore
index e97427608c1..42aeb55000a 100644
--- a/vendor/gitignore/Rails.gitignore
+++ b/vendor/gitignore/Rails.gitignore
@@ -8,7 +8,7 @@ capybara-*.html
/public/system
/coverage/
/spec/tmp
-**.orig
+*.orig
rerun.txt
pickle-email-*.html
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 57ed9f5d972..a0322dbd35a 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -148,6 +148,9 @@ _minted*
# pax
*.pax
+# pdfpcnotes
+*.pdfpc
+
# sagetex
*.sagetex.sage
*.sagetex.py
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index b829399ae85..eb83a8f122d 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -23,7 +23,6 @@ ExportedObj/
*.svd
*.pdb
-
# Unity3D generated meta files
*.pidb.meta
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index a752eacca7d..940794e60f2 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -219,6 +219,7 @@ UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
+*.ndf
# Business Intelligence projects
*.rdl.data
@@ -284,4 +285,4 @@ __pycache__/
*.btp.cs
*.btm.cs
*.odx.cs
-*.xsd.cs \ No newline at end of file
+*.xsd.cs
diff --git a/vendor/gitlab-ci-yml/CONTRIBUTING.md b/vendor/gitlab-ci-yml/CONTRIBUTING.md
new file mode 100644
index 00000000000..6e5160a2487
--- /dev/null
+++ b/vendor/gitlab-ci-yml/CONTRIBUTING.md
@@ -0,0 +1,5 @@
+The canonical repository for `.gitlab-ci.yml` templates is
+https://gitlab.com/gitlab-org/gitlab-ci-yml.
+
+GitLab only mirrors the templates. Please submit your merge requests to
+https://gitlab.com/gitlab-org/gitlab-ci-yml.
diff --git a/vendor/gitlab-ci-yml/Django.gitlab-ci.yml b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml
index b3106863cca..5ded2f5ce76 100644
--- a/vendor/gitlab-ci-yml/Django.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml
@@ -26,9 +26,24 @@ before_script:
# - apt-get update -q && apt-get install nodejs -yqq
- pip install -r requirements.txt
+# To get Django tests to work you may need to create a settings file using
+# the following DATABASES:
+#
+# DATABASES = {
+# 'default': {
+# 'ENGINE': 'django.db.backends.postgresql_psycopg2',
+# 'NAME': 'ci',
+# 'USER': 'postgres',
+# 'PASSWORD': 'postgres',
+# 'HOST': 'postgres',
+# 'PORT': '5432',
+# },
+# }
+#
+# and then adding `--settings app.settings.ci` (or similar) to the test command
+
test:
variables:
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
script:
- - python manage.py migrate
- python manage.py test
diff --git a/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
index d3bb388a1e7..636cb0a9a99 100644
--- a/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
@@ -41,7 +41,7 @@ review:
APP: $CI_COMMIT_REF_NAME
APP_HOST: $CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
environment:
- name: review/$CI_COMMIT_REF_SLUG
+ name: review/$CI_COMMIT_REF_NAME
url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
on_stop: stop-review
only:
@@ -59,7 +59,7 @@ stop-review:
APP: $CI_COMMIT_REF_NAME
GIT_STRATEGY: none
environment:
- name: review/$CI_COMMIT_REF_SLUG
+ name: review/$CI_COMMIT_REF_NAME
action: stop
only:
- branches
diff --git a/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml
index 908463c9d12..02d02250bbf 100644
--- a/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml
@@ -1,17 +1,16 @@
# Full project: https://gitlab.com/pages/hexo
-image: node:4.2.2
+image: node:6.10.0
pages:
- cache:
- paths:
- - node_modules/
-
script:
- - npm install hexo-cli -g
- npm install
- - hexo deploy
+ - ./node_modules/hexo/bin/hexo generate
artifacts:
paths:
- public
+ cache:
+ paths:
+ - node_modules
+ key: project
only:
- master
diff --git a/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
index d98cf94d635..37f50554036 100644
--- a/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
@@ -1,8 +1,10 @@
# Template project: https://gitlab.com/pages/jekyll
# Docs: https://docs.gitlab.com/ce/pages/
-# Jekyll version: 3.4.0
image: ruby:2.3
+variables:
+ JEKYLL_ENV: production
+
before_script:
- bundle install
@@ -25,4 +27,4 @@ pages:
- public
only:
- master
- \ No newline at end of file
+
diff --git a/vendor/gitlab-ci-yml/Scala.gitlab-ci.yml b/vendor/gitlab-ci-yml/Scala.gitlab-ci.yml
index 443ba42e38c..b4208ed9d7d 100644
--- a/vendor/gitlab-ci-yml/Scala.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Scala.gitlab-ci.yml
@@ -9,7 +9,7 @@ before_script:
- apt-get install apt-transport-https -yqq
# Add keyserver for SBT
- echo "deb http://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list
- - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 642AC823
+ - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823
# Install SBT
- apt-get update -yqq
- apt-get install sbt -yqq
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml
new file mode 100644
index 00000000000..555a51d35b9
--- /dev/null
+++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml
@@ -0,0 +1,84 @@
+# Explanation on the scripts:
+# https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md
+image: registry.gitlab.com/gitlab-examples/kubernetes-deploy
+
+variables:
+ # Application deployment domain
+ KUBE_DOMAIN: domain.example.com
+
+stages:
+ - build
+ - test
+ - review
+ - staging
+ - canary
+ - production
+ - cleanup
+
+build:
+ stage: build
+ script:
+ - command build
+ only:
+ - branches
+
+canary:
+ stage: canary
+ script:
+ - command canary
+ environment:
+ name: production
+ url: http://$CI_PROJECT_NAME.$KUBE_DOMAIN
+ when: manual
+ only:
+ - master
+
+production:
+ stage: production
+ script:
+ - command deploy
+ environment:
+ name: production
+ url: http://$CI_PROJECT_NAME.$KUBE_DOMAIN
+ when: manual
+ only:
+ - master
+
+staging:
+ stage: staging
+ script:
+ - command deploy
+ environment:
+ name: staging
+ url: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN
+ only:
+ - master
+
+review:
+ stage: review
+ script:
+ - command deploy
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ on_stop: stop_review
+ only:
+ - branches
+ except:
+ - master
+
+stop_review:
+ stage: cleanup
+ variables:
+ GIT_STRATEGY: none
+ script:
+ - command destroy
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ action: stop
+ when: manual
+ allow_failure: true
+ only:
+ - branches
+ except:
+ - master
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
index c644560647f..ee830ec2eb0 100644
--- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
@@ -23,8 +23,6 @@ build:
production:
stage: production
- variables:
- CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME.$KUBE_DOMAIN
script:
- command deploy
environment:
@@ -36,8 +34,6 @@ production:
staging:
stage: staging
- variables:
- CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN
script:
- command deploy
environment:
@@ -48,8 +44,6 @@ staging:
review:
stage: review
- variables:
- CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
script:
- command deploy
environment:
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index a2cbef126ad..6441df25fe1 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -1,9 +1,9 @@
RedCloth,4.3.2,MIT
abbrev,1.0.9,ISC
accepts,1.3.3,MIT
-ace-rails-ap,4.1.0,MIT
-acorn,4.0.4,MIT
-acorn-dynamic-import,2.0.1,MIT
+ace-rails-ap,4.1.2,MIT
+acorn,4.0.11,MIT
+acorn-dynamic-import,2.0.2,MIT
acorn-jsx,3.0.1,MIT
actionmailer,4.2.8,MIT
actionpack,4.2.8,MIT
@@ -16,19 +16,20 @@ acts-as-taggable-on,4.0.0,MIT
addressable,2.3.8,Apache 2.0
after,0.8.2,MIT
after_commit_queue,1.3.0,MIT
-ajv,4.11.2,MIT
+ajv,4.11.5,MIT
ajv-keywords,1.5.1,MIT
akismet,2.0.0,MIT
align-text,0.1.4,MIT
allocations,1.0.5,MIT
+alphanum-sort,1.0.2,MIT
amdefine,1.0.1,BSD-3-Clause OR MIT
ansi-escapes,1.4.0,MIT
-ansi-html,0.0.7,Apache 2.0
+ansi-html,0.0.5,"Apache, Version 2.0"
ansi-regex,2.1.1,MIT
ansi-styles,2.2.1,MIT
anymatch,1.3.0,ISC
append-transform,0.4.0,MIT
-aproba,1.1.0,ISC
+aproba,1.1.1,ISC
are-we-there-yet,1.1.2,ISC
arel,6.0.4,MIT
argparse,1.0.9,MIT
@@ -55,13 +56,14 @@ asynckit,0.4.0,MIT
attr_encrypted,3.0.3,MIT
attr_required,1.0.0,MIT
autoparse,0.3.3,Apache 2.0
+autoprefixer,6.7.7,MIT
autoprefixer-rails,6.2.3,MIT
aws-sign2,0.6.0,Apache 2.0
aws4,1.6.0,MIT
axiom-types,0.1.1,MIT
babel-code-frame,6.22.0,MIT
-babel-core,6.23.1,MIT
-babel-generator,6.23.0,MIT
+babel-core,6.24.0,MIT
+babel-generator,6.24.0,MIT
babel-helper-bindify-decorators,6.22.0,MIT
babel-helper-builder-binary-assignment-operator-visitor,6.22.0,MIT
babel-helper-call-delegate,6.22.0,MIT
@@ -76,10 +78,10 @@ babel-helper-regex,6.22.0,MIT
babel-helper-remap-async-to-generator,6.22.0,MIT
babel-helper-replace-supers,6.23.0,MIT
babel-helpers,6.23.0,MIT
-babel-loader,6.2.10,MIT
+babel-loader,6.4.1,MIT
babel-messages,6.23.0,MIT
babel-plugin-check-es2015-constants,6.22.0,MIT
-babel-plugin-istanbul,4.0.0,New BSD
+babel-plugin-istanbul,4.1.1,New BSD
babel-plugin-syntax-async-functions,6.13.0,MIT
babel-plugin-syntax-async-generators,6.13.0,MIT
babel-plugin-syntax-class-properties,6.13.0,MIT
@@ -92,6 +94,7 @@ babel-plugin-transform-async-generator-functions,6.22.0,MIT
babel-plugin-transform-async-to-generator,6.22.0,MIT
babel-plugin-transform-class-properties,6.23.0,MIT
babel-plugin-transform-decorators,6.22.0,MIT
+babel-plugin-transform-define,1.2.0,MIT
babel-plugin-transform-es2015-arrow-functions,6.22.0,MIT
babel-plugin-transform-es2015-block-scoped-functions,6.22.0,MIT
babel-plugin-transform-es2015-block-scoping,6.23.0,MIT
@@ -102,10 +105,10 @@ babel-plugin-transform-es2015-duplicate-keys,6.22.0,MIT
babel-plugin-transform-es2015-for-of,6.23.0,MIT
babel-plugin-transform-es2015-function-name,6.22.0,MIT
babel-plugin-transform-es2015-literals,6.22.0,MIT
-babel-plugin-transform-es2015-modules-amd,6.22.0,MIT
-babel-plugin-transform-es2015-modules-commonjs,6.23.0,MIT
+babel-plugin-transform-es2015-modules-amd,6.24.0,MIT
+babel-plugin-transform-es2015-modules-commonjs,6.24.0,MIT
babel-plugin-transform-es2015-modules-systemjs,6.23.0,MIT
-babel-plugin-transform-es2015-modules-umd,6.23.0,MIT
+babel-plugin-transform-es2015-modules-umd,6.24.0,MIT
babel-plugin-transform-es2015-object-super,6.22.0,MIT
babel-plugin-transform-es2015-parameters,6.23.0,MIT
babel-plugin-transform-es2015-shorthand-properties,6.22.0,MIT
@@ -118,16 +121,19 @@ babel-plugin-transform-exponentiation-operator,6.22.0,MIT
babel-plugin-transform-object-rest-spread,6.23.0,MIT
babel-plugin-transform-regenerator,6.22.0,MIT
babel-plugin-transform-strict-mode,6.22.0,MIT
-babel-preset-es2015,6.22.0,MIT
+babel-preset-es2015,6.24.0,MIT
+babel-preset-es2016,6.22.0,MIT
+babel-preset-es2017,6.22.0,MIT
+babel-preset-latest,6.24.0,MIT
babel-preset-stage-2,6.22.0,MIT
babel-preset-stage-3,6.22.0,MIT
-babel-register,6.23.0,MIT
-babel-runtime,6.22.0,MIT
+babel-register,6.24.0,MIT
+babel-runtime,6.23.0,MIT
babel-template,6.23.0,MIT
babel-traverse,6.23.1,MIT
babel-types,6.23.0,MIT
babosa,1.0.2,MIT
-babylon,6.15.0,MIT
+babylon,6.16.1,MIT
backo2,1.0.2,MIT
balanced-match,0.4.2,MIT
base32,0.3.2,MIT
@@ -143,21 +149,22 @@ binary-extensions,1.8.0,MIT
bindata,2.3.5,ruby
blob,0.0.4,unknown
block-stream,0.0.9,ISC
-bluebird,3.4.7,MIT
+bluebird,3.5.0,MIT
bn.js,4.11.6,MIT
-body-parser,1.16.0,MIT
+body-parser,1.17.1,MIT
boom,2.10.1,New BSD
bootstrap-sass,3.3.6,MIT
brace-expansion,1.1.6,MIT
braces,1.8.5,MIT
-brorand,1.0.7,MIT
+brorand,1.1.0,MIT
browser,2.2.0,MIT
browserify-aes,1.0.6,MIT
browserify-cipher,1.0.0,MIT
browserify-des,1.0.0,MIT
browserify-rsa,4.0.1,MIT
-browserify-sign,4.0.0,ISC
+browserify-sign,4.0.4,ISC
browserify-zlib,0.1.4,MIT
+browserslist,1.7.7,MIT
buffer,4.9.1,MIT
buffer-shims,1.0.0,MIT
buffer-xor,1.0.3,MIT
@@ -169,8 +176,10 @@ caller-path,0.1.0,MIT
callsite,1.0.0,unknown
callsites,0.2.0,MIT
camelcase,1.2.1,MIT
+caniuse-api,1.6.1,MIT
+caniuse-db,1.0.30000649,CC-BY-4.0
carrierwave,0.11.2,MIT
-caseless,0.11.0,Apache 2.0
+caseless,0.12.0,Apache 2.0
cause,0.1,MIT
center-align,0.1.3,MIT
chalk,1.1.3,MIT
@@ -181,16 +190,24 @@ chronic_duration,0.10.6,MIT
chunky_png,1.3.5,MIT
cipher-base,1.0.3,MIT
circular-json,0.3.1,MIT
+citrus,3.0.2,MIT
+clap,1.1.3,MIT
cli-cursor,1.0.2,MIT
cli-width,2.1.0,ISC
cliui,2.1.0,ISC
clone,1.0.2,MIT
co,4.6.0,MIT
+coa,1.0.1,MIT
code-point-at,1.1.0,MIT
coercible,1.0.0,MIT
coffee-rails,4.1.1,MIT
coffee-script,2.4.1,MIT
coffee-script-source,1.10.0,MIT
+color,0.11.4,MIT
+color-convert,1.9.0,MIT
+color-name,1.1.2,MIT
+color-string,0.3.0,MIT
+colormin,1.1.2,MIT
colors,1.1.2,MIT
combine-lists,1.0.1,MIT
combined-stream,1.0.5,MIT
@@ -199,26 +216,29 @@ commondir,1.0.1,MIT
component-bind,1.0.0,unknown
component-emitter,1.2.1,MIT
component-inherit,0.0.3,unknown
-compressible,2.0.9,MIT
+compressible,2.0.10,MIT
compression,1.6.2,MIT
compression-webpack-plugin,0.3.2,MIT
concat-map,0.0.1,MIT
concat-stream,1.6.0,MIT
-concurrent-ruby,1.0.4,MIT
-connect,3.5.0,MIT
+config-chain,1.1.11,MIT
+configstore,1.4.0,Simplified BSD
+connect,3.6.0,MIT
connect-history-api-fallback,1.3.0,MIT
connection_pool,2.2.1,MIT
console-browserify,1.1.0,MIT
console-control-strings,1.1.0,ISC
+consolidate,0.14.5,MIT
constants-browserify,1.0.0,MIT
contains-path,0.1.0,MIT
content-disposition,0.5.2,MIT
content-type,1.0.2,MIT
-convert-source-map,1.3.0,MIT
+convert-source-map,1.5.0,MIT
cookie,0.3.1,MIT
cookie-signature,1.0.6,MIT
core-js,2.4.1,MIT
core-util-is,1.0.2,MIT
+cosmiconfig,2.1.1,MIT
crack,0.4.3,MIT
create-ecdh,4.0.0,MIT
create-hash,1.1.2,MIT
@@ -226,14 +246,21 @@ create-hmac,1.1.4,MIT
creole,0.5.0,ruby
cryptiles,2.0.5,New BSD
crypto-browserify,3.11.0,MIT
+css-color-names,0.0.4,MIT
+css-loader,0.28.0,MIT
+css-selector-tokenizer,0.7.0,MIT
css_parser,1.4.1,MIT
+cssesc,0.1.0,MIT
+cssnano,3.10.0,MIT
+csso,2.3.2,MIT
custom-event,1.0.1,MIT
-d,0.1.1,MIT
-d3,3.5.11,New BSD
+d,1.0.0,MIT
+d3,3.5.17,New BSD
d3_rails,3.5.11,MIT
dashdash,1.14.1,MIT
date-now,0.1.4,MIT
-debug,2.6.0,MIT
+de-indent,1.0.2,MIT
+debug,2.6.3,MIT
decamelize,1.2.0,MIT
deckar01-task_list,1.0.6,MIT
deep-extend,0.4.1,MIT
@@ -241,6 +268,7 @@ deep-is,0.1.3,MIT
default-require-extensions,1.0.0,MIT
default_value_for,3.0.2,MIT
defaults,1.0.3,MIT
+defined,1.0.0,MIT
del,2.2.2,MIT
delayed-stream,1.0.0,MIT
delegates,1.0.0,MIT
@@ -255,62 +283,74 @@ di,0.0.1,MIT
diff-lcs,1.2.5,"MIT,Perl Artistic v2,GNU GPL v2"
diffie-hellman,5.0.2,MIT
diffy,3.1.0,MIT
-doctrine,1.5.0,BSD
-document-register-element,1.3.0,MIT
+doctrine,2.0.0,Apache 2.0
+document-register-element,1.4.1,MIT
dom-serialize,2.2.1,MIT
+dom-serializer,0.1.0,MIT
domain-browser,1.1.7,MIT
domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0"
+domelementtype,1.3.0,unknown
+domhandler,2.3.0,unknown
+domutils,1.5.1,unknown
doorkeeper,4.2.0,MIT
doorkeeper-openid_connect,1.1.2,MIT
-dropzone,4.2.0,MIT
+dropzone,4.3.0,MIT
dropzonejs-rails,0.7.2,MIT
duplexer,0.1.1,MIT
+duplexify,3.5.0,MIT
ecc-jsbn,0.1.1,MIT
+editorconfig,0.13.2,MIT
ee-first,1.1.1,MIT
ejs,2.5.6,Apache 2.0
-elliptic,6.3.3,MIT
+electron-to-chromium,1.3.3,ISC
+elliptic,6.4.0,MIT
email_reply_trimmer,0.1.6,MIT
emoji-unicode-version,0.2.1,MIT
emojis-list,2.1.0,MIT
encodeurl,1.0.1,MIT
encryptor,3.0.0,MIT
-engine.io,1.8.2,MIT
-engine.io-client,1.8.2,MIT
+end-of-stream,1.0.0,MIT
+engine.io,1.8.3,MIT
+engine.io-client,1.8.3,MIT
engine.io-parser,1.3.2,MIT
enhanced-resolve,3.1.0,MIT
ent,2.2.0,MIT
+entities,1.1.1,BSD-like
equalizer,0.0.11,MIT
errno,0.1.4,MIT
-error-ex,1.3.0,MIT
+error-ex,1.3.1,MIT
erubis,2.7.0,MIT
-es5-ext,0.10.12,MIT
-es6-iterator,2.0.0,MIT
-es6-map,0.1.4,MIT
-es6-promise,4.0.5,MIT
-es6-set,0.1.4,MIT
-es6-symbol,3.1.0,MIT
-es6-weak-map,2.0.1,MIT
+es5-ext,0.10.15,MIT
+es6-iterator,2.0.1,MIT
+es6-map,0.1.5,MIT
+es6-promise,3.0.2,MIT
+es6-set,0.1.5,MIT
+es6-symbol,3.1.1,MIT
+es6-weak-map,2.0.2,MIT
escape-html,1.0.3,MIT
escape-string-regexp,1.0.5,MIT
escape_utils,1.1.1,MIT
escodegen,1.8.1,Simplified BSD
escope,3.6.0,Simplified BSD
-eslint,3.15.0,MIT
+eslint,3.19.0,MIT
eslint-config-airbnb-base,10.0.1,MIT
eslint-import-resolver-node,0.2.3,MIT
eslint-import-resolver-webpack,0.8.1,MIT
eslint-module-utils,2.0.0,MIT
eslint-plugin-filenames,1.1.0,MIT
+eslint-plugin-html,2.0.1,ISC
eslint-plugin-import,2.2.0,MIT
eslint-plugin-jasmine,2.2.0,MIT
-espree,3.4.0,Simplified BSD
-esprima,3.1.3,Simplified BSD
+espree,3.4.1,Simplified BSD
+esprima,2.7.3,Simplified BSD
+esquery,1.0.0,BSD
esrecurse,4.1.0,Simplified BSD
estraverse,4.1.1,Simplified BSD
esutils,2.0.2,BSD
-etag,1.7.0,MIT
+etag,1.8.0,MIT
eve-raphael,0.5.0,Apache 2.0
-event-emitter,0.3.4,MIT
+event-emitter,0.3.5,MIT
+event-stream,3.3.4,MIT
eventemitter3,1.2.0,MIT
events,1.1.1,MIT
eventsource,0.1.6,MIT
@@ -321,7 +361,7 @@ exit-hook,1.1.1,MIT
expand-braces,0.1.2,MIT
expand-brackets,0.1.5,MIT
expand-range,1.8.2,MIT
-express,4.14.1,MIT
+express,4.15.2,MIT
expression_parser,0.9.0,MIT
extend,3.0.0,MIT
extglob,0.3.2,MIT
@@ -332,20 +372,23 @@ faraday,0.9.2,MIT
faraday_middleware,0.10.0,MIT
faraday_middleware-multi_json,0.0.6,MIT
fast-levenshtein,2.0.6,MIT
-faye-websocket,0.10.0,MIT
+fastparse,1.1.1,MIT
+faye-websocket,0.7.3,MIT
fd-slicer,1.0.1,MIT
ffi,1.9.10,BSD
figures,1.7.0,MIT
file-entry-cache,2.0.0,MIT
+file-loader,0.11.1,MIT
filename-regex,2.0.0,MIT
fileset,2.0.3,MIT
-filesize,3.5.4,New BSD
+filesize,3.3.0,New BSD
fill-range,2.2.3,MIT
-finalhandler,0.5.1,MIT
+finalhandler,1.0.1,MIT
find-cache-dir,0.1.1,MIT
find-root,0.1.2,MIT
find-up,2.1.0,MIT
flat-cache,1.2.2,MIT
+flatten,1.0.2,MIT
flowdock,0.7.1,MIT
fog-aws,0.11.0,MIT
fog-core,1.42.0,MIT
@@ -356,20 +399,21 @@ fog-openstack,0.1.6,MIT
fog-rackspace,0.1.1,MIT
fog-xml,0.1.2,MIT
font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License"
-for-in,0.1.6,MIT
-for-own,0.1.4,MIT
+for-in,1.0.2,MIT
+for-own,0.1.5,MIT
forever-agent,0.6.1,Apache 2.0
form-data,2.1.2,MIT
formatador,0.2.5,MIT
forwarded,0.1.0,MIT
-fresh,0.3.0,MIT
+fresh,0.5.0,MIT
+from,0.1.7,MIT
fs-extra,1.0.0,MIT
fs.realpath,1.0.0,ISC
fsevents,,unknown
-fstream,1.0.10,ISC
+fstream,1.0.11,ISC
fstream-ignore,1.0.5,ISC
function-bind,1.1.0,MIT
-gauge,2.7.2,ISC
+gauge,2.7.3,ISC
gemnasium-gitlab-service,0.2.6,MIT
gemojione,3.0.1,MIT
generate-function,2.0.0,MIT
@@ -377,7 +421,7 @@ generate-object-property,1.2.0,MIT
get-caller-file,1.0.2,ISC
get_process_mem,0.2.0,MIT
getpass,0.1.6,MIT
-gitaly,0.2.1,MIT
+gitaly,0.5.0,MIT
github-linguist,4.7.6,MIT
github-markup,1.4.0,MIT
gitlab-flowdock-git-hook,1.0.1,MIT
@@ -388,15 +432,16 @@ glob,7.1.1,ISC
glob-base,0.3.0,MIT
glob-parent,2.0.0,ISC
globalid,0.3.7,MIT
-globals,9.14.0,MIT
+globals,9.17.0,MIT
globby,5.0.0,MIT
gollum-grit_adapter,1.0.1,MIT
gollum-lib,4.2.1,MIT
-gollum-rugged_adapter,0.4.2,MIT
+gollum-rugged_adapter,0.4.4,MIT
gon,6.1.0,MIT
google-api-client,0.8.7,Apache 2.0
-google-protobuf,3.2.0,New BSD
+google-protobuf,3.2.0.2,New BSD
googleauth,0.5.1,Apache 2.0
+got,3.3.1,MIT
graceful-fs,4.1.11,ISC
graceful-readlink,1.0.1,MIT
grape,0.19.1,MIT
@@ -406,34 +451,40 @@ gzip-size,3.0.0,MIT
hamlit,2.6.1,MIT
handle-thing,1.2.5,MIT
handlebars,4.0.6,MIT
-har-validator,2.0.6,ISC
+har-schema,1.0.5,ISC
+har-validator,4.2.1,ISC
has,1.0.1,MIT
has-ansi,2.0.0,MIT
has-binary,0.1.7,MIT
has-cors,1.1.0,MIT
has-flag,1.0.0,MIT
has-unicode,2.0.1,ISC
+hash-sum,1.0.2,MIT
hash.js,1.0.3,MIT
hasha,2.2.0,MIT
hashie,3.5.5,MIT
hawk,3.1.3,New BSD
+he,1.1.1,MIT
health_check,2.6.0,MIT
hipchat,1.5.2,MIT
+hmac-drbg,1.0.0,MIT
hoek,2.16.3,New BSD
home-or-tmp,2.0.0,MIT
-hosted-git-info,2.2.0,ISC
+hosted-git-info,2.4.1,ISC
hpack.js,2.1.6,MIT
+html-comment-regex,1.1.1,MIT
html-entities,1.2.0,MIT
html-pipeline,1.11.0,MIT
html2text,0.2.0,MIT
htmlentities,4.3.4,MIT
+htmlparser2,3.9.2,MIT
http,0.9.8,MIT
http-cookie,1.0.3,MIT
http-deceiver,1.2.7,MIT
-http-errors,1.5.1,MIT
+http-errors,1.6.1,MIT
http-form_data,1.0.1,MIT
http-proxy,1.16.2,MIT
-http-proxy-middleware,0.17.3,MIT
+http-proxy-middleware,0.17.4,MIT
http-signature,1.1.1,MIT
http_parser.rb,0.6.0,MIT
httparty,0.13.7,MIT
@@ -442,24 +493,30 @@ https-browserify,0.0.1,MIT
i18n,0.8.1,MIT
ice_nine,0.11.2,MIT
iconv-lite,0.4.15,MIT
+icss-replace-symbols,1.0.2,ISC
ieee754,1.1.8,New BSD
-ignore,3.2.2,MIT
+ignore,3.2.6,MIT
+ignore-by-default,1.0.1,ISC
+immediate,3.0.6,MIT
imurmurhash,0.1.4,MIT
+indexes-of,1.0.1,MIT
indexof,0.0.1,unknown
+infinity-agent,2.0.3,MIT
inflight,1.0.6,ISC
influxdb,0.2.3,MIT
inherits,2.0.3,ISC
ini,1.3.4,ISC
inquirer,0.12.0,MIT
-interpret,1.0.1,MIT
+interpret,1.0.2,MIT
invariant,2.2.2,New BSD
invert-kv,1.0.0,MIT
-ipaddr.js,1.2.0,MIT
+ipaddr.js,1.3.0,MIT
ipaddress,0.8.3,MIT
is-absolute,0.2.6,MIT
+is-absolute-url,2.1.0,MIT
is-arrayish,0.2.1,MIT
is-binary-path,1.0.1,MIT
-is-buffer,1.1.4,MIT
+is-buffer,1.1.5,MIT
is-builtin-module,1.0.0,MIT
is-dotfile,1.0.2,MIT
is-equal-shallow,0.1.3,MIT
@@ -468,46 +525,52 @@ is-extglob,1.0.0,MIT
is-finite,1.0.2,MIT
is-fullwidth-code-point,1.0.0,MIT
is-glob,2.0.1,MIT
-is-my-json-valid,2.15.0,MIT
+is-my-json-valid,2.16.0,MIT
+is-npm,1.0.0,MIT
is-number,2.1.0,MIT
is-path-cwd,1.0.0,MIT
is-path-in-cwd,1.0.0,MIT
is-path-inside,1.0.0,MIT
+is-plain-obj,1.1.0,MIT
is-posix-bracket,0.1.1,MIT
is-primitive,2.0.0,MIT
is-property,1.0.2,MIT
+is-redirect,1.0.0,MIT
is-relative,0.2.1,MIT
is-resolvable,1.0.0,MIT
is-stream,1.1.0,MIT
+is-svg,2.1.0,MIT
is-typedarray,1.0.0,MIT
is-unc-path,0.1.2,MIT
is-utf8,0.2.1,MIT
is-windows,0.2.0,MIT
isarray,1.0.0,MIT
isbinaryfile,3.0.2,MIT
-isexe,1.1.2,ISC
+isexe,2.0.0,ISC
isobject,2.1.0,MIT
isstream,0.1.2,MIT
istanbul,0.4.5,New BSD
-istanbul-api,1.1.1,New BSD
-istanbul-lib-coverage,1.0.1,New BSD
-istanbul-lib-hook,1.0.0,New BSD
-istanbul-lib-instrument,1.4.2,New BSD
-istanbul-lib-report,1.0.0-alpha.3,New BSD
-istanbul-lib-source-maps,1.1.0,New BSD
-istanbul-reports,1.0.1,New BSD
+istanbul-api,1.1.7,New BSD
+istanbul-lib-coverage,1.0.2,New BSD
+istanbul-lib-hook,1.0.5,New BSD
+istanbul-lib-instrument,1.7.0,New BSD
+istanbul-lib-report,1.0.0,New BSD
+istanbul-lib-source-maps,1.1.1,New BSD
+istanbul-reports,1.0.2,New BSD
jasmine-core,2.5.2,MIT
jasmine-jquery,2.1.1,MIT
jira-ruby,1.1.2,MIT
jodid25519,1.0.2,MIT
-jquery,2.2.1,MIT
+jquery,2.2.4,MIT
jquery-atwho-rails,1.3.2,MIT
jquery-rails,4.1.1,MIT
-jquery-ujs,1.2.1,MIT
-js-cookie,2.1.3,MIT
+jquery-ujs,1.2.2,MIT
+js-base64,2.1.9,BSD
+js-beautify,1.6.12,MIT
+js-cookie,2.1.4,MIT
js-tokens,3.0.1,MIT
-js-yaml,3.8.1,MIT
-jsbn,0.1.0,BSD
+js-yaml,3.7.0,MIT
+jsbn,0.1.1,MIT
jsesc,1.3.0,MIT
json,1.8.6,ruby
json-jwt,1.7.1,MIT
@@ -520,51 +583,72 @@ json5,0.5.1,MIT
jsonfile,2.4.0,MIT
jsonify,0.0.0,Public Domain
jsonpointer,4.0.1,MIT
-jsprim,1.3.1,MIT
+jsprim,1.4.0,MIT
+jszip,3.1.3,(MIT OR GPL-3.0)
+jszip-utils,0.0.2,MIT or GPLv3
jwt,1.5.6,MIT
kaminari,0.17.0,MIT
-karma,1.4.1,MIT
-karma-coverage-istanbul-reporter,0.2.0,MIT
+karma,1.6.0,MIT
+karma-coverage-istanbul-reporter,0.2.3,MIT
karma-jasmine,1.1.0,MIT
-karma-mocha-reporter,2.2.2,MIT
-karma-phantomjs-launcher,1.0.2,MIT
+karma-mocha-reporter,2.2.3,MIT
+karma-phantomjs-launcher,1.0.4,MIT
karma-sourcemap-loader,0.3.7,MIT
-karma-webpack,2.0.2,MIT
+karma-webpack,2.0.3,MIT
kew,0.7.0,Apache 2.0
kgio,2.10.0,LGPL-2.1+
kind-of,3.1.0,MIT
klaw,1.3.1,MIT
kubeclient,2.2.0,MIT
+latest-version,1.0.1,MIT
launchy,2.4.3,ISC
lazy-cache,1.0.4,MIT
lcid,1.0.0,MIT
levn,0.3.0,MIT
licensee,8.7.0,MIT
+lie,3.1.1,MIT
little-plugger,1.1.4,MIT
load-json-file,1.1.0,MIT
loader-runner,2.3.0,MIT
-loader-utils,0.2.16,MIT
+loader-utils,0.2.17,MIT
locate-path,2.0.0,MIT
lodash,4.17.4,MIT
+lodash._baseassign,3.2.0,MIT
+lodash._basecopy,3.0.1,MIT
lodash._baseget,3.7.2,MIT
+lodash._bindcallback,3.0.1,MIT
+lodash._createassigner,3.1.1,MIT
+lodash._getnative,3.9.1,MIT
+lodash._isiterateecall,3.0.9,MIT
lodash._topath,3.8.1,MIT
-lodash.camelcase,4.1.1,MIT
+lodash.assign,3.2.0,MIT
+lodash.camelcase,4.3.0,MIT
lodash.capitalize,4.2.1,MIT
lodash.cond,4.5.2,MIT
lodash.deburr,4.1.0,MIT
-lodash.get,3.7.0,MIT
+lodash.defaults,3.1.2,MIT
+lodash.get,4.4.2,MIT
+lodash.isarguments,3.1.0,MIT
lodash.isarray,3.0.4,MIT
lodash.kebabcase,4.0.1,MIT
+lodash.keys,3.1.2,MIT
+lodash.memoize,4.1.2,MIT
+lodash.restparam,3.6.1,MIT
lodash.snakecase,4.0.1,MIT
+lodash.uniq,4.5.0,MIT
lodash.words,4.2.0,MIT
log4js,0.6.38,Apache 2.0
logging,2.1.0,MIT
longest,1.0.1,MIT
loofah,2.0.3,MIT
loose-envify,1.3.1,MIT
-lru-cache,2.2.4,MIT
+lowercase-keys,1.0.0,MIT
+lru-cache,3.2.0,ISC
+macaddress,0.2.8,MIT
mail,2.6.4,MIT
mail_room,0.9.1,MIT
+map-stream,0.1.0,unknown
+math-expression-evaluator,1.2.16,MIT
media-typer,0.3.0,MIT
memoist,0.15.0,MIT
memory-fs,0.4.1,MIT
@@ -574,16 +658,17 @@ methods,1.1.2,MIT
micromatch,2.3.11,MIT
miller-rabin,4.0.0,MIT
mime,1.3.4,MIT
-mime-db,1.26.0,MIT
+mime-db,1.27.0,MIT
mime-types,2.99.3,"MIT,Artistic-2.0,GPL-2.0"
mimemagic,0.3.0,MIT
mini_portile2,2.1.0,MIT
minimalistic-assert,1.0.0,ISC
+minimalistic-crypto-utils,1.0.1,MIT
minimatch,3.0.3,ISC
minimist,0.0.8,MIT
mkdirp,0.5.1,MIT
-moment,2.17.1,MIT
-mousetrap,1.4.6,Apache 2.0
+moment,2.18.1,MIT
+mousetrap,1.6.1,Apache 2.0
mousetrap-rails,1.4.6,"MIT,Apache"
ms,0.7.2,MIT
multi_json,1.12.1,MIT
@@ -595,17 +680,22 @@ mute-stream,0.0.5,ISC
nan,2.5.1,MIT
natural-compare,1.4.0,MIT
negotiator,0.6.1,MIT
+nested-error-stacks,1.0.2,MIT
net-ldap,0.12.1,MIT
net-ssh,3.0.1,MIT
netrc,0.11.0,MIT
node-libs-browser,2.0.0,MIT
-node-pre-gyp,0.6.33,New BSD
+node-pre-gyp,0.6.34,New BSD
node-zopfli,2.0.2,MIT
+nodemon,1.11.0,MIT
nokogiri,1.6.8.1,MIT
-nopt,3.0.6,ISC
-normalize-package-data,2.3.5,Simplified BSD
-normalize-path,2.0.1,MIT
+nopt,4.0.1,ISC
+normalize-package-data,2.3.6,Simplified BSD
+normalize-path,2.1.1,MIT
+normalize-range,0.1.2,MIT
+normalize-url,1.9.1,MIT
npmlog,4.0.2,ISC
+num2fraction,1.2.2,MIT
number-is-nan,1.0.1,MIT
numerizer,0.1.1,MIT
oauth,0.5.1,MIT
@@ -637,7 +727,7 @@ omniauth-twitter,1.2.1,MIT
omniauth_crowd,2.2.3,MIT
on-finished,2.3.0,MIT
on-headers,1.0.1,MIT
-once,1.3.3,ISC
+once,1.4.0,ISC
onetime,1.1.0,MIT
opener,1.4.3,(WTFPL OR MIT)
opn,4.0.2,MIT
@@ -652,11 +742,13 @@ os-browserify,0.2.1,MIT
os-homedir,1.0.2,MIT
os-locale,1.4.0,MIT
os-tmpdir,1.0.2,MIT
+osenv,0.1.4,ISC
p-limit,1.1.0,MIT
p-locate,2.0.0,MIT
-pako,0.2.9,MIT
+package-json,1.2.0,MIT
+pako,1.0.5,(MIT AND Zlib)
paranoia,2.2.0,MIT
-parse-asn1,5.0.0,ISC
+parse-asn1,5.1.0,ISC
parse-glob,3.0.4,MIT
parse-json,2.2.0,MIT
parsejson,0.0.3,MIT
@@ -670,8 +762,10 @@ path-is-inside,1.0.2,(WTFPL OR MIT)
path-parse,1.0.5,MIT
path-to-regexp,0.1.7,MIT
path-type,1.1.0,MIT
+pause-stream,0.0.11,"Apache2,MIT"
pbkdf2,3.0.9,MIT
pend,1.2.0,MIT
+performance-now,0.2.0,MIT
pg,0.18.4,"BSD,ruby,GPL"
phantomjs-prebuilt,2.1.14,Apache 2.0
pify,2.3.0,MIT
@@ -683,21 +777,63 @@ pkg-up,1.0.0,MIT
pluralize,1.2.1,MIT
portfinder,1.0.13,MIT
posix-spawn,0.3.11,"MIT,LGPL"
+postcss,5.2.16,MIT
+postcss-calc,5.3.1,MIT
+postcss-colormin,2.2.2,MIT
+postcss-convert-values,2.6.1,MIT
+postcss-discard-comments,2.0.4,MIT
+postcss-discard-duplicates,2.1.0,MIT
+postcss-discard-empty,2.1.0,MIT
+postcss-discard-overridden,0.1.1,MIT
+postcss-discard-unused,2.2.3,MIT
+postcss-filter-plugins,2.0.2,MIT
+postcss-load-config,1.2.0,MIT
+postcss-load-options,1.2.0,MIT
+postcss-load-plugins,2.3.0,MIT
+postcss-merge-idents,2.1.7,MIT
+postcss-merge-longhand,2.0.2,MIT
+postcss-merge-rules,2.1.2,MIT
+postcss-message-helpers,2.0.0,MIT
+postcss-minify-font-values,1.0.5,MIT
+postcss-minify-gradients,1.0.5,MIT
+postcss-minify-params,1.2.2,MIT
+postcss-minify-selectors,2.1.1,MIT
+postcss-modules-extract-imports,1.0.1,ISC
+postcss-modules-local-by-default,1.1.1,MIT
+postcss-modules-scope,1.0.2,ISC
+postcss-modules-values,1.2.2,ISC
+postcss-normalize-charset,1.1.1,MIT
+postcss-normalize-url,3.0.8,MIT
+postcss-ordered-values,2.2.3,MIT
+postcss-reduce-idents,2.4.0,MIT
+postcss-reduce-initial,1.0.1,MIT
+postcss-reduce-transforms,1.0.4,MIT
+postcss-selector-parser,2.2.3,MIT
+postcss-svgo,2.1.6,MIT
+postcss-unique-selectors,2.0.2,MIT
+postcss-value-parser,3.3.0,MIT
+postcss-zindex,2.2.0,MIT
prelude-ls,1.1.2,MIT
premailer,1.8.6,New BSD
premailer-rails,1.9.2,MIT
+prepend-http,1.0.4,MIT
preserve,0.2.0,MIT
private,0.1.7,MIT
process,0.11.9,MIT
process-nextick-args,1.0.7,MIT
progress,1.1.8,MIT
-proxy-addr,1.1.3,MIT
+proto-list,1.2.4,ISC
+proxy-addr,1.1.4,MIT
prr,0.0.0,MIT
+ps-tree,1.1.0,MIT
+pseudomap,1.0.2,ISC
public-encrypt,4.0.0,MIT
punycode,1.4.1,MIT
pyu-ruby-sasl,0.0.3.3,MIT
+q,1.5.0,MIT
qjobs,1.1.5,MIT
-qs,6.2.0,New BSD
+qs,6.4.0,New BSD
+query-string,4.3.2,MIT
querystring,0.2.0,MIT
querystring-es3,0.2.1,MIT
querystringify,0.0.4,MIT
@@ -723,16 +859,19 @@ range-parser,1.2.0,MIT
raphael,2.2.7,MIT
raw-body,2.2.0,MIT
raw-loader,0.5.1,MIT
-rc,1.1.6,(BSD-2-Clause OR MIT OR Apache-2.0)
+rc,1.2.1,(BSD-2-Clause OR MIT OR Apache-2.0)
rdoc,4.2.2,ruby
+react-dev-utils,0.5.2,New BSD
+read-all-stream,3.1.0,MIT
read-pkg,1.1.0,MIT
read-pkg-up,1.0.1,MIT
-readable-stream,2.1.5,MIT
+readable-stream,2.0.6,MIT
readdirp,2.1.0,MIT
readline2,1.0.1,MIT
recaptcha,3.0.0,MIT
rechoir,0.6.2,MIT
recursive-open-struct,1.0.0,MIT
+recursive-readdir,2.1.1,MIT
redcarpet,3.4.0,MIT
redis,3.2.2,MIT
redis-actionpack,5.0.1,MIT
@@ -741,31 +880,36 @@ redis-namespace,1.5.2,MIT
redis-rack,1.6.0,MIT
redis-rails,5.0.1,MIT
redis-store,1.2.0,MIT
+reduce-css-calc,1.3.0,MIT
+reduce-function-call,1.0.2,MIT
regenerate,1.3.2,MIT
-regenerator-runtime,0.10.1,MIT
+regenerator-runtime,0.10.3,MIT
regenerator-transform,0.9.8,BSD
regex-cache,0.4.3,MIT
regexpu-core,2.0.0,MIT
+registry-url,3.1.0,MIT
regjsgen,0.2.0,MIT
regjsparser,0.1.5,BSD
+remove-trailing-separator,1.0.1,ISC
repeat-element,1.1.2,MIT
repeat-string,1.6.1,MIT
repeating,2.0.1,MIT
-request,2.79.0,Apache 2.0
+request,2.81.0,Apache 2.0
request-progress,2.0.1,MIT
request_store,1.3.1,MIT
require-directory,2.1.1,MIT
+require-from-string,1.2.1,MIT
require-main-filename,1.0.1,ISC
require-uncached,1.0.3,MIT
requires-port,1.0.0,MIT
-resolve,1.2.0,MIT
+resolve,1.3.2,MIT
resolve-from,1.0.1,MIT
responders,2.3.0,MIT
rest-client,2.0.0,MIT
restore-cursor,1.0.1,MIT
retriable,1.4.1,MIT
right-align,0.1.3,MIT
-rimraf,2.5.4,ISC
+rimraf,2.6.1,ISC
rinku,2.0.0,ISC
ripemd160,1.0.1,New BSD
rotp,2.1.2,MIT
@@ -778,7 +922,7 @@ ruby-saml,1.4.1,MIT
rubyntlm,0.5.2,MIT
rubypants,0.2.0,BSD
rufus-scheduler,3.1.10,MIT
-rugged,0.24.0,MIT
+rugged,0.25.1.1,MIT
run-async,0.1.0,MIT
rx-lite,3.1.2,Apache 2.0
safe-buffer,5.0.1,MIT
@@ -787,158 +931,190 @@ sanitize,2.1.0,MIT
sass,3.4.22,MIT
sass-rails,5.0.6,MIT
sawyer,0.8.1,MIT
+sax,1.2.2,ISC
securecompare,1.0.0,MIT
seed-fu,2.3.6,MIT
select-hose,2.0.0,MIT
select2,3.5.2-browserify,unknown
select2-rails,3.5.9.3,MIT
semver,5.3.0,ISC
-send,0.14.2,MIT
-sentry-raven,2.0.2,Apache 2.0
+semver-diff,2.1.0,MIT
+send,0.15.1,MIT
+sentry-raven,2.4.0,Apache 2.0
serve-index,1.8.0,MIT
-serve-static,1.11.2,MIT
+serve-static,1.12.1,MIT
set-blocking,2.0.0,ISC
set-immediate-shim,1.0.1,MIT
setimmediate,1.0.5,MIT
-setprototypeof,1.0.2,ISC
+setprototypeof,1.0.3,ISC
settingslogic,2.0.9,MIT
sha.js,2.4.8,MIT
-shelljs,0.7.6,New BSD
+shelljs,0.7.7,New BSD
sidekiq,4.2.7,LGPL
sidekiq-cron,0.4.4,MIT
sidekiq-limit_fetch,3.4.0,MIT
+sigmund,1.0.1,ISC
signal-exit,3.0.2,ISC
signet,0.7.3,Apache 2.0
slack-notifier,1.5.1,MIT
slash,1.0.0,MIT
slice-ansi,0.0.4,MIT
+slide,1.1.6,ISC
sntp,1.0.9,BSD
-socket.io,1.7.2,MIT
+socket.io,1.7.3,MIT
socket.io-adapter,0.5.0,MIT
-socket.io-client,1.7.2,MIT
+socket.io-client,1.7.3,MIT
socket.io-parser,2.3.1,MIT
sockjs,0.3.18,MIT
-sockjs-client,1.1.1,MIT
+sockjs-client,1.0.1,MIT
+sort-keys,1.1.2,MIT
source-list-map,0.1.8,MIT
source-map,0.5.6,New BSD
-source-map-support,0.4.11,MIT
+source-map-support,0.4.14,MIT
spdx-correct,1.0.2,Apache 2.0
spdx-expression-parse,1.0.4,(MIT AND CC-BY-3.0)
spdx-license-ids,1.2.2,Unlicense
spdy,3.4.4,MIT
spdy-transport,2.0.18,MIT
+split,0.3.3,MIT
sprintf-js,1.0.3,New BSD
sprockets,3.7.1,MIT
sprockets-rails,3.2.0,MIT
-sshpk,1.10.2,MIT
+sshpk,1.11.0,MIT
state_machines,0.4.0,MIT
state_machines-activemodel,0.4.0,MIT
state_machines-activerecord,0.4.0,MIT
stats-webpack-plugin,0.4.3,MIT
statuses,1.3.1,MIT
stream-browserify,2.0.1,MIT
-stream-http,2.6.3,MIT
+stream-combiner,0.0.4,MIT
+stream-http,2.7.0,MIT
+stream-shift,1.0.0,MIT
+strict-uri-encode,1.1.0,MIT
+string-length,1.0.1,MIT
string-width,1.0.2,MIT
-string.fromcodepoint,0.2.1,MIT
-string.prototype.codepointat,0.2.0,MIT
string_decoder,0.10.31,MIT
stringex,2.5.2,MIT
stringstream,0.0.5,MIT
strip-ansi,3.0.1,MIT
strip-bom,2.0.0,MIT
-strip-json-comments,1.0.4,MIT
-supports-color,0.2.0,MIT
+strip-json-comments,2.0.1,MIT
+supports-color,3.2.3,MIT
+svgo,0.7.2,MIT
sys-filesystem,1.1.6,Artistic 2.0
table,3.8.3,New BSD
tapable,0.2.6,MIT
tar,2.2.1,ISC
-tar-pack,3.3.0,Simplified BSD
+tar-pack,3.4.0,Simplified BSD
temple,0.7.7,MIT
-test-exclude,4.0.0,ISC
+test-exclude,4.0.3,ISC
text-table,0.2.0,MIT
thor,0.19.4,MIT
thread_safe,0.3.6,Apache 2.0
+three,0.84.0,MIT
+three-orbit-controls,82.1.0,MIT
+three-stl-loader,1.0.4,MIT
throttleit,1.0.0,MIT
through,2.3.8,MIT
tilt,2.0.6,MIT
timeago.js,2.0.5,MIT
+timed-out,2.0.0,MIT
timers-browserify,2.0.2,MIT
timfel-krb5-auth,0.8.3,LGPL
-tmp,0.0.28,MIT
+tmp,0.0.31,MIT
to-array,0.1.4,MIT
to-arraybuffer,1.0.1,MIT
to-fast-properties,1.0.2,MIT
+toml-rb,0.3.15,MIT
tool,0.2.3,MIT
+touch,1.0.0,ISC
tough-cookie,2.3.2,New BSD
+traverse,0.6.6,MIT
trim-right,1.0.1,MIT
truncato,0.7.8,MIT
tryit,1.0.3,MIT
tty-browserify,0.0.0,MIT
-tunnel-agent,0.4.3,Apache 2.0
+tunnel-agent,0.6.0,Apache 2.0
tweetnacl,0.14.5,Unlicense
type-check,0.3.2,MIT
-type-is,1.6.14,MIT
+type-is,1.6.15,MIT
typedarray,0.0.6,MIT
tzinfo,1.2.2,MIT
u2f,0.2.1,MIT
uglifier,2.7.2,MIT
-uglify-js,2.7.5,Simplified BSD
+uglify-js,2.8.21,Simplified BSD
uglify-to-browserify,1.0.2,MIT
uid-number,0.0.6,ISC
ultron,1.0.2,MIT
unc-path-regex,0.1.2,MIT
+undefsafe,0.0.3,MIT / http://rem.mit-license.org
underscore,1.8.3,MIT
underscore-rails,1.8.3,MIT
unf,0.1.4,BSD
unf_ext,0.0.7.2,MIT
unicorn,5.1.0,ruby
unicorn-worker-killer,0.4.4,ruby
+uniq,1.0.1,MIT
+uniqid,4.1.1,MIT
+uniqs,2.0.0,MIT
unpipe,1.0.0,MIT
+update-notifier,0.5.0,Simplified BSD
url,0.11.0,MIT
url-parse,1.0.5,MIT
url_safe_base64,0.2.2,MIT
user-home,2.0.0,MIT
-useragent,2.1.12,MIT
+useragent,2.1.13,MIT
util,0.10.3,MIT
util-deprecate,1.0.2,MIT
utils-merge,1.0.0,MIT
uuid,3.0.1,MIT
validate-npm-package-license,3.0.1,Apache 2.0
validates_hostname,1.0.6,MIT
-vary,1.1.0,MIT
+vary,1.1.1,MIT
+vendors,1.0.1,MIT
verror,1.3.6,MIT
version_sorter,2.1.0,MIT
virtus,1.0.5,MIT
+visibilityjs,1.2.4,MIT
vm-browserify,0.0.4,MIT
vmstat,2.3.0,MIT
void-elements,2.0.1,MIT
-vue,2.1.10,MIT
+vue,2.2.6,MIT
+vue-hot-reload-api,2.0.11,MIT
+vue-loader,11.3.4,MIT
vue-resource,0.9.3,MIT
+vue-style-loader,2.0.5,MIT
+vue-template-compiler,2.2.6,MIT
+vue-template-es2015-compiler,1.5.2,MIT
warden,1.2.6,MIT
-watchpack,1.2.1,MIT
+watchpack,1.3.1,MIT
wbuf,1.7.2,MIT
-webpack,2.2.1,MIT
-webpack-bundle-analyzer,2.3.0,MIT
-webpack-dev-middleware,1.10.0,MIT
-webpack-dev-server,2.3.0,MIT
-webpack-rails,0.9.9,MIT
-webpack-sources,0.1.4,MIT
+webpack,2.3.3,MIT
+webpack-bundle-analyzer,2.3.1,MIT
+webpack-dev-middleware,1.10.1,MIT
+webpack-dev-server,2.4.2,MIT
+webpack-rails,0.9.10,MIT
+webpack-sources,0.1.5,MIT
websocket-driver,0.6.5,MIT
websocket-extensions,0.1.1,MIT
-which,1.2.12,ISC
+whet.extend,0.9.9,MIT
+which,1.2.14,ISC
which-module,1.0.0,ISC
wide-align,1.1.0,ISC
wikicloth,0.8.1,MIT
window-size,0.1.0,MIT
-wordwrap,0.0.2,MIT/X11
+wordwrap,1.0.0,MIT
wrap-ansi,2.1.0,MIT
wrappy,1.0.2,ISC
write,0.2.1,MIT
-ws,1.1.1,MIT
+write-file-atomic,1.3.1,ISC
+ws,1.1.2,MIT
wtf-8,1.0.0,MIT
+xdg-basedir,2.0.0,MIT
xmlhttprequest-ssl,1.5.3,MIT
xtend,4.0.1,MIT
y18n,3.2.1,ISC
+yallist,2.1.2,ISC
yargs,3.10.0,MIT
yargs-parser,4.2.1,ISC
yauzl,2.4.1,MIT
diff --git a/yarn.lock b/yarn.lock
index 2434b3a8a48..90ba39a3251 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -60,6 +60,10 @@ align-text@^0.1.1, align-text@^0.1.3:
longest "^1.0.1"
repeat-string "^1.5.2"
+alphanum-sort@^1.0.1, alphanum-sort@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+
amdefine@>=0.0.4:
version "1.0.1"
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
@@ -68,6 +72,10 @@ ansi-escapes@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
+ansi-html@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.5.tgz#0dcaa5a081206866bc240a3b773a184ea3b88b64"
+
ansi-html@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
@@ -206,6 +214,17 @@ asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+autoprefixer@^6.3.1:
+ version "6.7.7"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"
+ dependencies:
+ browserslist "^1.7.6"
+ caniuse-db "^1.0.30000634"
+ normalize-range "^0.1.2"
+ num2fraction "^1.2.2"
+ postcss "^5.2.16"
+ postcss-value-parser "^3.2.3"
+
aws-sign2@~0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
@@ -214,7 +233,7 @@ aws4@^1.2.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
-babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
+babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
dependencies:
@@ -805,7 +824,7 @@ backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
-balanced-match@^0.4.1:
+balanced-match@^0.4.1, balanced-match@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
@@ -855,7 +874,7 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
-bluebird@^3.3.0:
+bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.3.0:
version "3.4.7"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
@@ -964,6 +983,13 @@ browserify-zlib@^0.1.4:
dependencies:
pako "~0.2.0"
+browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
+ version "1.7.7"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9"
+ dependencies:
+ caniuse-db "^1.0.30000639"
+ electron-to-chromium "^1.2.7"
+
buffer-shims@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51"
@@ -1018,6 +1044,19 @@ camelcase@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+caniuse-api@^1.5.2:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c"
+ dependencies:
+ browserslist "^1.3.6"
+ caniuse-db "^1.0.30000529"
+ lodash.memoize "^4.1.2"
+ lodash.uniq "^4.5.0"
+
+caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
+ version "1.0.30000649"
+ resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000649.tgz#1ee1754a6df235450c8b7cd15e0ebf507221a86a"
+
caseless@~0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
@@ -1064,6 +1103,12 @@ circular-json@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d"
+clap@^1.0.9:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/clap/-/clap-1.1.3.tgz#b3bd36e93dd4cbfb395a3c26896352445265c05b"
+ dependencies:
+ chalk "^1.1.3"
+
cli-cursor@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
@@ -1098,11 +1143,49 @@ co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+coa@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.1.tgz#7f959346cfc8719e3f7233cd6852854a7c67d8a3"
+ dependencies:
+ q "^1.1.2"
+
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
-colors@^1.1.0:
+color-convert@^1.3.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
+ dependencies:
+ color-name "^1.1.1"
+
+color-name@^1.0.0, color-name@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.2.tgz#5c8ab72b64bd2215d617ae9559ebb148475cf98d"
+
+color-string@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991"
+ dependencies:
+ color-name "^1.0.0"
+
+color@^0.11.0:
+ version "0.11.4"
+ resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764"
+ dependencies:
+ clone "^1.0.2"
+ color-convert "^1.3.0"
+ color-string "^0.3.0"
+
+colormin@^1.0.5:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133"
+ dependencies:
+ color "^0.11.0"
+ css-color-names "0.0.4"
+ has "^1.0.1"
+
+colors@^1.1.0, colors@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
@@ -1190,6 +1273,26 @@ concat-stream@^1.4.6:
readable-stream "^2.2.2"
typedarray "^0.0.6"
+config-chain@~1.1.5:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2"
+ dependencies:
+ ini "^1.3.4"
+ proto-list "~1.2.1"
+
+configstore@^1.0.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021"
+ dependencies:
+ graceful-fs "^4.1.2"
+ mkdirp "^0.5.0"
+ object-assign "^4.0.1"
+ os-tmpdir "^1.0.0"
+ osenv "^0.1.0"
+ uuid "^2.0.1"
+ write-file-atomic "^1.1.2"
+ xdg-basedir "^2.0.0"
+
connect-history-api-fallback@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169"
@@ -1213,6 +1316,12 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+consolidate@^0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63"
+ dependencies:
+ bluebird "^3.1.1"
+
constants-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@@ -1253,6 +1362,17 @@ core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.1.1.tgz#817f2c2039347a1e9bf7d090c0923e53f749ca82"
+ dependencies:
+ js-yaml "^3.4.3"
+ minimist "^1.2.0"
+ object-assign "^4.1.0"
+ os-homedir "^1.0.1"
+ parse-json "^2.2.0"
+ require-from-string "^1.1.0"
+
create-ecdh@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
@@ -1297,6 +1417,91 @@ crypto-browserify@^3.11.0:
public-encrypt "^4.0.0"
randombytes "^2.0.0"
+css-color-names@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
+
+css-loader@^0.28.0:
+ version "0.28.0"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.0.tgz#417cfa9789f8cde59a30ccbf3e4da7a806889bad"
+ dependencies:
+ babel-code-frame "^6.11.0"
+ css-selector-tokenizer "^0.7.0"
+ cssnano ">=2.6.1 <4"
+ loader-utils "^1.0.2"
+ lodash.camelcase "^4.3.0"
+ object-assign "^4.0.1"
+ postcss "^5.0.6"
+ postcss-modules-extract-imports "^1.0.0"
+ postcss-modules-local-by-default "^1.0.1"
+ postcss-modules-scope "^1.0.0"
+ postcss-modules-values "^1.1.0"
+ source-list-map "^0.1.7"
+
+css-selector-tokenizer@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.6.0.tgz#6445f582c7930d241dcc5007a43d6fcb8f073152"
+ dependencies:
+ cssesc "^0.1.0"
+ fastparse "^1.1.1"
+ regexpu-core "^1.0.0"
+
+css-selector-tokenizer@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86"
+ dependencies:
+ cssesc "^0.1.0"
+ fastparse "^1.1.1"
+ regexpu-core "^1.0.0"
+
+cssesc@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
+
+"cssnano@>=2.6.1 <4":
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38"
+ dependencies:
+ autoprefixer "^6.3.1"
+ decamelize "^1.1.2"
+ defined "^1.0.0"
+ has "^1.0.1"
+ object-assign "^4.0.1"
+ postcss "^5.0.14"
+ postcss-calc "^5.2.0"
+ postcss-colormin "^2.1.8"
+ postcss-convert-values "^2.3.4"
+ postcss-discard-comments "^2.0.4"
+ postcss-discard-duplicates "^2.0.1"
+ postcss-discard-empty "^2.0.1"
+ postcss-discard-overridden "^0.1.1"
+ postcss-discard-unused "^2.2.1"
+ postcss-filter-plugins "^2.0.0"
+ postcss-merge-idents "^2.1.5"
+ postcss-merge-longhand "^2.0.1"
+ postcss-merge-rules "^2.0.3"
+ postcss-minify-font-values "^1.0.2"
+ postcss-minify-gradients "^1.0.1"
+ postcss-minify-params "^1.0.4"
+ postcss-minify-selectors "^2.0.4"
+ postcss-normalize-charset "^1.1.0"
+ postcss-normalize-url "^3.0.7"
+ postcss-ordered-values "^2.1.0"
+ postcss-reduce-idents "^2.2.2"
+ postcss-reduce-initial "^1.0.0"
+ postcss-reduce-transforms "^1.0.3"
+ postcss-svgo "^2.1.1"
+ postcss-unique-selectors "^2.0.2"
+ postcss-value-parser "^3.2.3"
+ postcss-zindex "^2.0.1"
+
+csso@~2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85"
+ dependencies:
+ clap "^1.0.9"
+ source-map "^0.5.3"
+
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
@@ -1321,6 +1526,10 @@ date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+de-indent@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
+
debug@0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39"
@@ -1337,13 +1546,13 @@ debug@2.3.3:
dependencies:
ms "0.7.2"
-debug@2.6.0, debug@^2.1.1, debug@^2.2.0:
+debug@2.6.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b"
dependencies:
ms "0.7.2"
-decamelize@^1.0.0, decamelize@^1.1.1:
+decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -1367,6 +1576,10 @@ defaults@^1.0.2:
dependencies:
clone "^1.0.2"
+defined@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+
del@^2.0.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
@@ -1440,24 +1653,70 @@ dom-serialize@^2.2.0:
extend "^3.0.0"
void-elements "^2.0.0"
+dom-serializer@0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
+ dependencies:
+ domelementtype "~1.1.1"
+ entities "~1.1.1"
+
domain-browser@^1.1.1:
version "1.1.7"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
+domelementtype@1, domelementtype@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
+
+domelementtype@~1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
+
+domhandler@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738"
+ dependencies:
+ domelementtype "1"
+
+domutils@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
dropzone@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3"
-duplexer@^0.1.1:
+duplexer@^0.1.1, duplexer@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
+duplexify@^3.2.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.0.tgz#1aa773002e1578457e9d9d4a50b0ccaaebcbd604"
+ dependencies:
+ end-of-stream "1.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+ stream-shift "^1.0.0"
+
ecc-jsbn@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
dependencies:
jsbn "~0.1.0"
+editorconfig@^0.13.2:
+ version "0.13.2"
+ resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.13.2.tgz#8e57926d9ee69ab6cb999f027c2171467acceb35"
+ dependencies:
+ bluebird "^3.0.5"
+ commander "^2.9.0"
+ lru-cache "^3.2.0"
+ sigmund "^1.0.1"
+
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -1466,6 +1725,10 @@ ejs@^2.5.5:
version "2.5.6"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88"
+electron-to-chromium@^1.2.7:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.3.tgz#651eb63fe89f39db70ffc8dbd5d9b66958bc6a0e"
+
elliptic@^6.0.0:
version "6.3.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.3.tgz#5482d9646d54bcb89fd7d994fc9e2e9568876e3f"
@@ -1487,6 +1750,12 @@ encodeurl@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
+end-of-stream@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.0.0.tgz#d4596e702734a93e40e9af864319eabd99ff2f0e"
+ dependencies:
+ once "~1.3.0"
+
engine.io-client@1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.2.tgz#c38767547f2a7d184f5752f6f0ad501006703766"
@@ -1547,6 +1816,10 @@ ent@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
+entities@^1.1.1, entities@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
+
errno@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
@@ -1585,7 +1858,7 @@ es6-map@^0.1.3:
es6-symbol "~3.1.0"
event-emitter "~0.3.4"
-es6-promise@~3.0.2:
+es6-promise@^3.0.2, es6-promise@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6"
@@ -1623,7 +1896,7 @@ escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
-escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -1690,6 +1963,12 @@ eslint-plugin-filenames@^1.1.0:
lodash.kebabcase "4.0.1"
lodash.snakecase "4.0.1"
+eslint-plugin-html@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-2.0.1.tgz#3a829510e82522f1e2e44d55d7661a176121fce1"
+ dependencies:
+ htmlparser2 "^3.8.2"
+
eslint-plugin-import@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.2.0.tgz#72ba306fad305d67c4816348a4699a4229ac8b4e"
@@ -1709,6 +1988,10 @@ eslint-plugin-jasmine@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.2.0.tgz#7135879383c39a667c721d302b9f20f0389543de"
+eslint-plugin-promise@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.5.0.tgz#78fbb6ffe047201627569e85a6c5373af2a68fca"
+
eslint@^3.10.1:
version "3.15.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.15.0.tgz#bdcc6a6c5ffe08160e7b93c066695362a91e30f2"
@@ -1755,7 +2038,7 @@ espree@^3.4.0:
acorn "4.0.4"
acorn-jsx "^3.0.0"
-esprima@2.7.x, esprima@^2.7.1:
+esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1:
version "2.7.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
@@ -1801,6 +2084,18 @@ event-emitter@~0.3.4:
d "~0.1.1"
es5-ext "~0.10.7"
+event-stream@~3.3.0:
+ version "3.3.4"
+ resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
+ dependencies:
+ duplexer "~0.1.1"
+ from "~0"
+ map-stream "~0.1.0"
+ pause-stream "0.0.11"
+ split "0.3"
+ stream-combiner "~0.0.4"
+ through "~2.3.1"
+
eventemitter3@1.x.x:
version "1.2.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
@@ -1809,7 +2104,7 @@ events@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
-eventsource@0.1.6:
+eventsource@0.1.6, eventsource@^0.1.3:
version "0.1.6"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232"
dependencies:
@@ -1910,6 +2205,10 @@ fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+fastparse@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
+
faye-websocket@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
@@ -1922,6 +2221,12 @@ faye-websocket@~0.11.0:
dependencies:
websocket-driver ">=0.5.1"
+faye-websocket@~0.7.3:
+ version "0.7.3"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.7.3.tgz#cc4074c7f4a4dfd03af54dd65c354b135132ce11"
+ dependencies:
+ websocket-driver ">=0.3.6"
+
fd-slicer@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
@@ -1959,6 +2264,10 @@ fileset@^2.0.2:
glob "^7.0.3"
minimatch "^3.0.3"
+filesize@3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.3.0.tgz#53149ea3460e3b2e024962a51648aa572cf98122"
+
filesize@^3.5.4:
version "3.5.4"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.4.tgz#742fc7fb6aef4ee3878682600c22f840731e1fda"
@@ -2027,6 +2336,10 @@ flat-cache@^1.2.1:
graceful-fs "^4.1.2"
write "^0.2.1"
+flatten@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
+
for-in@^0.1.5:
version "0.1.6"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8"
@@ -2057,6 +2370,10 @@ fresh@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f"
+from@~0:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
+
fs-extra@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950"
@@ -2180,7 +2497,22 @@ globby@^5.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
-graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+got@^3.2.0:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca"
+ dependencies:
+ duplexify "^3.2.0"
+ infinity-agent "^2.0.0"
+ is-redirect "^1.0.0"
+ is-stream "^1.0.0"
+ lowercase-keys "^1.0.0"
+ nested-error-stacks "^1.0.0"
+ object-assign "^3.0.0"
+ prepend-http "^1.0.0"
+ read-all-stream "^3.0.0"
+ timed-out "^2.0.0"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
@@ -2188,7 +2520,7 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
-gzip-size@^3.0.0:
+gzip-size@3.0.0, gzip-size@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
dependencies:
@@ -2247,6 +2579,10 @@ has@^1.0.1:
dependencies:
function-bind "^1.0.2"
+hash-sum@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
+
hash.js@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.0.3.tgz#1332ff00156c0a0ffdd8236013d07b77a0451573"
@@ -2269,6 +2605,10 @@ hawk@~3.1.3:
hoek "2.x.x"
sntp "1.x.x"
+he@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
+
hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
@@ -2293,10 +2633,25 @@ hpack.js@^2.1.6:
readable-stream "^2.0.1"
wbuf "^1.1.0"
-html-entities@^1.2.0:
+html-comment-regex@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
+
+html-entities@1.2.0, html-entities@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2"
+htmlparser2@^3.8.2:
+ version "3.9.2"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
+ dependencies:
+ domelementtype "^1.3.0"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^2.0.2"
+
http-deceiver@^1.2.4:
version "1.2.7"
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@@ -2341,10 +2696,18 @@ iconv-lite@0.4.15:
version "0.4.15"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
+icss-replace-symbols@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.0.2.tgz#cb0b6054eb3af6edc9ab1d62d01933e2d4c8bfa5"
+
ieee754@^1.1.4:
version "1.1.8"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
+ignore-by-default@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
+
ignore@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410"
@@ -2357,10 +2720,18 @@ imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+indexes-of@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+
indexof@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+infinity-agent@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/infinity-agent/-/infinity-agent-2.0.3.tgz#45e0e2ff7a9eb030b27d62b74b3744b7a7ac4216"
+
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -2376,7 +2747,7 @@ inherits@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
-ini@~1.3.0:
+ini@^1.3.4, ini@~1.3.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
@@ -2416,6 +2787,10 @@ ipaddr.js@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.2.0.tgz#8aba49c9192799585bdd643e0ccb50e8ae777ba4"
+is-absolute-url@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
+
is-absolute@^0.2.3:
version "0.2.6"
resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.2.6.tgz#20de69f3db942ef2d87b9c2da36f172235b1b5eb"
@@ -2502,6 +2877,10 @@ is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4:
jsonpointer "^4.0.0"
xtend "^4.0.0"
+is-npm@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
+
is-number@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806"
@@ -2528,6 +2907,10 @@ is-path-inside@^1.0.0:
dependencies:
path-is-inside "^1.0.1"
+is-plain-obj@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+
is-posix-bracket@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
@@ -2540,6 +2923,10 @@ is-property@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
+is-redirect@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
+
is-relative@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5"
@@ -2552,10 +2939,16 @@ is-resolvable@^1.0.0:
dependencies:
tryit "^1.0.1"
-is-stream@^1.0.1:
+is-stream@^1.0.0, is-stream@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+is-svg@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9"
+ dependencies:
+ html-comment-regex "^1.1.0"
+
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -2707,6 +3100,19 @@ jquery@>=1.8.0, jquery@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f"
+js-base64@^2.1.9:
+ version "2.1.9"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce"
+
+js-beautify@^1.6.3:
+ version "1.6.12"
+ resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.6.12.tgz#78b75933505d376da6e5a28e9b7887e0094db8b5"
+ dependencies:
+ config-chain "~1.1.5"
+ editorconfig "^0.13.2"
+ mkdirp "~0.5.0"
+ nopt "~3.0.1"
+
js-cookie@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.1.3.tgz#48071625217ac9ecfab8c343a13d42ec09ff0526"
@@ -2715,13 +3121,20 @@ js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
-js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0:
+js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0:
version "3.8.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628"
dependencies:
argparse "^1.0.7"
esprima "^3.1.1"
+js-yaml@~3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^2.6.0"
+
jsbn@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd"
@@ -2883,6 +3296,12 @@ klaw@^1.0.0:
optionalDependencies:
graceful-fs "^4.1.9"
+latest-version@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-1.0.1.tgz#72cfc46e3e8d1be651e1ebb54ea9f6ea96f374bb"
+ dependencies:
+ package-json "^1.0.0"
+
lazy-cache@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
@@ -2929,7 +3348,7 @@ loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
json5 "^0.5.0"
object-assign "^4.0.1"
-loader-utils@^1.0.2:
+loader-utils@^1.0.2, loader-utils@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
dependencies:
@@ -2944,16 +3363,55 @@ locate-path@^2.0.0:
p-locate "^2.0.0"
path-exists "^3.0.0"
+lodash._baseassign@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e"
+ dependencies:
+ lodash._basecopy "^3.0.0"
+ lodash.keys "^3.0.0"
+
+lodash._basecopy@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
+
lodash._baseget@^3.0.0:
version "3.7.2"
resolved "https://registry.yarnpkg.com/lodash._baseget/-/lodash._baseget-3.7.2.tgz#1b6ae1d5facf3c25532350a13c1197cb8bb674f4"
+lodash._bindcallback@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
+
+lodash._createassigner@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11"
+ dependencies:
+ lodash._bindcallback "^3.0.0"
+ lodash._isiterateecall "^3.0.0"
+ lodash.restparam "^3.0.0"
+
+lodash._getnative@^3.0.0:
+ version "3.9.1"
+ resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
+
+lodash._isiterateecall@^3.0.0:
+ version "3.0.9"
+ resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c"
+
lodash._topath@^3.0.0:
version "3.8.1"
resolved "https://registry.yarnpkg.com/lodash._topath/-/lodash._topath-3.8.1.tgz#3ec5e2606014f4cb97f755fe6914edd8bfc00eac"
dependencies:
lodash.isarray "^3.0.0"
+lodash.assign@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa"
+ dependencies:
+ lodash._baseassign "^3.0.0"
+ lodash._createassigner "^3.0.0"
+ lodash.keys "^3.0.0"
+
lodash.camelcase@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.1.1.tgz#065b3ff08f0b7662f389934c46a5504c90e0b2d8"
@@ -2962,6 +3420,10 @@ lodash.camelcase@4.1.1:
lodash.deburr "^4.0.0"
lodash.words "^4.0.0"
+lodash.camelcase@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
+
lodash.capitalize@^4.0.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9"
@@ -2974,6 +3436,13 @@ lodash.deburr@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
+lodash.defaults@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-3.1.2.tgz#c7308b18dbf8bc9372d701a73493c61192bd2e2c"
+ dependencies:
+ lodash.assign "^3.0.0"
+ lodash.restparam "^3.0.0"
+
lodash.get@4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -2985,6 +3454,10 @@ lodash.get@^3.7.0:
lodash._baseget "^3.0.0"
lodash._topath "^3.0.0"
+lodash.isarguments@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
+
lodash.isarray@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
@@ -2996,6 +3469,22 @@ lodash.kebabcase@4.0.1:
lodash.deburr "^4.0.0"
lodash.words "^4.0.0"
+lodash.keys@^3.0.0:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
+ dependencies:
+ lodash._getnative "^3.0.0"
+ lodash.isarguments "^3.0.0"
+ lodash.isarray "^3.0.0"
+
+lodash.memoize@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+
+lodash.restparam@^3.0.0:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
+
lodash.snakecase@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.0.1.tgz#bd012e5d2f93f7b58b9303e9a7fbfd5db13d6281"
@@ -3003,6 +3492,10 @@ lodash.snakecase@4.0.1:
lodash.deburr "^4.0.0"
lodash.words "^4.0.0"
+lodash.uniq@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+
lodash.words@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.words/-/lodash.words-4.2.0.tgz#5ecfeaf8ecf8acaa8e0c8386295f1993c9cf4036"
@@ -3032,10 +3525,39 @@ loose-envify@^1.0.0:
dependencies:
js-tokens "^3.0.0"
+lowercase-keys@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
+
lru-cache@2.2.x:
version "2.2.4"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
+lru-cache@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-3.2.0.tgz#71789b3b7f5399bec8565dda38aa30d2a097efee"
+ dependencies:
+ pseudomap "^1.0.1"
+
+lru-cache@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e"
+ dependencies:
+ pseudomap "^1.0.1"
+ yallist "^2.0.0"
+
+macaddress@^0.2.8:
+ version "0.2.8"
+ resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
+
+map-stream@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
+
+math-expression-evaluator@^1.2.14:
+ version "1.2.16"
+ resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.16.tgz#b357fa1ca9faefb8e48d10c14ef2bcb2d9f0a7c9"
+
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -3102,7 +3624,7 @@ minimalistic-assert@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
-"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3:
+"minimatch@2 || 3", minimatch@3.0.3, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
dependencies:
@@ -3160,6 +3682,12 @@ negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+nested-error-stacks@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz#19f619591519f096769a5ba9a86e6eeec823c3cf"
+ dependencies:
+ inherits "~2.0.1"
+
node-libs-browser@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-1.1.1.tgz#2a38243abedd7dffcd07a97c9aca5668975a6fea"
@@ -3239,12 +3767,33 @@ node-zopfli@^2.0.0:
nan "^2.0.0"
node-pre-gyp "^0.6.4"
-nopt@3.x, nopt@~3.0.6:
+nodemon@^1.11.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.11.0.tgz#226c562bd2a7b13d3d7518b49ad4828a3623d06c"
+ dependencies:
+ chokidar "^1.4.3"
+ debug "^2.2.0"
+ es6-promise "^3.0.2"
+ ignore-by-default "^1.0.0"
+ lodash.defaults "^3.1.2"
+ minimatch "^3.0.0"
+ ps-tree "^1.0.1"
+ touch "1.0.0"
+ undefsafe "0.0.3"
+ update-notifier "0.5.0"
+
+nopt@3.x, nopt@~3.0.1, nopt@~3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
dependencies:
abbrev "1"
+nopt@~1.0.10:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
+ dependencies:
+ abbrev "1"
+
normalize-package-data@^2.3.2:
version "2.3.5"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df"
@@ -3258,6 +3807,19 @@ normalize-path@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a"
+normalize-range@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+
+normalize-url@^1.4.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
+ dependencies:
+ object-assign "^4.0.1"
+ prepend-http "^1.0.0"
+ query-string "^4.1.0"
+ sort-keys "^1.0.0"
+
npmlog@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f"
@@ -3267,6 +3829,10 @@ npmlog@^4.0.1:
gauge "~2.7.1"
set-blocking "~2.0.0"
+num2fraction@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+
number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@@ -3279,6 +3845,10 @@ object-assign@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"
+object-assign@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
+
object-assign@^4.0.1, object-assign@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -3314,7 +3884,7 @@ once@1.x, once@^1.3.0, once@^1.4.0:
dependencies:
wrappy "1"
-once@~1.3.3:
+once@~1.3.0, once@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20"
dependencies:
@@ -3367,7 +3937,7 @@ os-browserify@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
-os-homedir@^1.0.0:
+os-homedir@^1.0.0, os-homedir@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
@@ -3377,10 +3947,17 @@ os-locale@^1.4.0:
dependencies:
lcid "^1.0.0"
-os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+osenv@^0.1.0:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
p-limit@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
@@ -3391,6 +3968,13 @@ p-locate@^2.0.0:
dependencies:
p-limit "^1.1.0"
+package-json@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0"
+ dependencies:
+ got "^3.2.0"
+ registry-url "^3.0.0"
+
pako@~0.2.0:
version "0.2.9"
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
@@ -3484,6 +4068,12 @@ path-type@^1.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
+pause-stream@0.0.11:
+ version "0.0.11"
+ resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
+ dependencies:
+ through "~2.3"
+
pbkdf2@^3.0.3:
version "3.0.9"
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.9.tgz#f2c4b25a600058b3c3773c086c37dbbee1ffe693"
@@ -3552,10 +4142,275 @@ portfinder@^1.0.9:
debug "^2.2.0"
mkdirp "0.5.x"
+postcss-calc@^5.2.0:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e"
+ dependencies:
+ postcss "^5.0.2"
+ postcss-message-helpers "^2.0.0"
+ reduce-css-calc "^1.2.6"
+
+postcss-colormin@^2.1.8:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b"
+ dependencies:
+ colormin "^1.0.5"
+ postcss "^5.0.13"
+ postcss-value-parser "^3.2.3"
+
+postcss-convert-values@^2.3.4:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d"
+ dependencies:
+ postcss "^5.0.11"
+ postcss-value-parser "^3.1.2"
+
+postcss-discard-comments@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d"
+ dependencies:
+ postcss "^5.0.14"
+
+postcss-discard-duplicates@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932"
+ dependencies:
+ postcss "^5.0.4"
+
+postcss-discard-empty@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5"
+ dependencies:
+ postcss "^5.0.14"
+
+postcss-discard-overridden@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58"
+ dependencies:
+ postcss "^5.0.16"
+
+postcss-discard-unused@^2.2.1:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433"
+ dependencies:
+ postcss "^5.0.14"
+ uniqs "^2.0.0"
+
+postcss-filter-plugins@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c"
+ dependencies:
+ postcss "^5.0.4"
+ uniqid "^4.0.0"
+
+postcss-load-config@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a"
+ dependencies:
+ cosmiconfig "^2.1.0"
+ object-assign "^4.1.0"
+ postcss-load-options "^1.2.0"
+ postcss-load-plugins "^2.3.0"
+
+postcss-load-options@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c"
+ dependencies:
+ cosmiconfig "^2.1.0"
+ object-assign "^4.1.0"
+
+postcss-load-plugins@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92"
+ dependencies:
+ cosmiconfig "^2.1.1"
+ object-assign "^4.1.0"
+
+postcss-merge-idents@^2.1.5:
+ version "2.1.7"
+ resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270"
+ dependencies:
+ has "^1.0.1"
+ postcss "^5.0.10"
+ postcss-value-parser "^3.1.1"
+
+postcss-merge-longhand@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658"
+ dependencies:
+ postcss "^5.0.4"
+
+postcss-merge-rules@^2.0.3:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721"
+ dependencies:
+ browserslist "^1.5.2"
+ caniuse-api "^1.5.2"
+ postcss "^5.0.4"
+ postcss-selector-parser "^2.2.2"
+ vendors "^1.0.0"
+
+postcss-message-helpers@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e"
+
+postcss-minify-font-values@^1.0.2:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69"
+ dependencies:
+ object-assign "^4.0.1"
+ postcss "^5.0.4"
+ postcss-value-parser "^3.0.2"
+
+postcss-minify-gradients@^1.0.1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1"
+ dependencies:
+ postcss "^5.0.12"
+ postcss-value-parser "^3.3.0"
+
+postcss-minify-params@^1.0.4:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3"
+ dependencies:
+ alphanum-sort "^1.0.1"
+ postcss "^5.0.2"
+ postcss-value-parser "^3.0.2"
+ uniqs "^2.0.0"
+
+postcss-minify-selectors@^2.0.4:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf"
+ dependencies:
+ alphanum-sort "^1.0.2"
+ has "^1.0.1"
+ postcss "^5.0.14"
+ postcss-selector-parser "^2.0.0"
+
+postcss-modules-extract-imports@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.0.1.tgz#8fb3fef9a6dd0420d3f6d4353cf1ff73f2b2a341"
+ dependencies:
+ postcss "^5.0.4"
+
+postcss-modules-local-by-default@^1.0.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.1.1.tgz#29a10673fa37d19251265ca2ba3150d9040eb4ce"
+ dependencies:
+ css-selector-tokenizer "^0.6.0"
+ postcss "^5.0.4"
+
+postcss-modules-scope@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.0.2.tgz#ff977395e5e06202d7362290b88b1e8cd049de29"
+ dependencies:
+ css-selector-tokenizer "^0.6.0"
+ postcss "^5.0.4"
+
+postcss-modules-values@^1.1.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.2.2.tgz#f0e7d476fe1ed88c5e4c7f97533a3e772ad94ca1"
+ dependencies:
+ icss-replace-symbols "^1.0.2"
+ postcss "^5.0.14"
+
+postcss-normalize-charset@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1"
+ dependencies:
+ postcss "^5.0.5"
+
+postcss-normalize-url@^3.0.7:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222"
+ dependencies:
+ is-absolute-url "^2.0.0"
+ normalize-url "^1.4.0"
+ postcss "^5.0.14"
+ postcss-value-parser "^3.2.3"
+
+postcss-ordered-values@^2.1.0:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d"
+ dependencies:
+ postcss "^5.0.4"
+ postcss-value-parser "^3.0.1"
+
+postcss-reduce-idents@^2.2.2:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3"
+ dependencies:
+ postcss "^5.0.4"
+ postcss-value-parser "^3.0.2"
+
+postcss-reduce-initial@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea"
+ dependencies:
+ postcss "^5.0.4"
+
+postcss-reduce-transforms@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1"
+ dependencies:
+ has "^1.0.1"
+ postcss "^5.0.8"
+ postcss-value-parser "^3.0.1"
+
+postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90"
+ dependencies:
+ flatten "^1.0.2"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss-svgo@^2.1.1:
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d"
+ dependencies:
+ is-svg "^2.0.0"
+ postcss "^5.0.14"
+ postcss-value-parser "^3.2.3"
+ svgo "^0.7.0"
+
+postcss-unique-selectors@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d"
+ dependencies:
+ alphanum-sort "^1.0.1"
+ postcss "^5.0.4"
+ uniqs "^2.0.0"
+
+postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
+
+postcss-zindex@^2.0.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22"
+ dependencies:
+ has "^1.0.1"
+ postcss "^5.0.4"
+ uniqs "^2.0.0"
+
+postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.21, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16:
+ version "5.2.16"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.16.tgz#732b3100000f9ff8379a48a53839ed097376ad57"
+ dependencies:
+ chalk "^1.1.3"
+ js-base64 "^2.1.9"
+ source-map "^0.5.6"
+ supports-color "^3.2.3"
+
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+prepend-http@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
+
preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
@@ -3576,6 +4431,10 @@ progress@^1.1.8, progress@~1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
+proto-list@~1.2.1:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
+
proxy-addr@~1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074"
@@ -3587,6 +4446,16 @@ prr@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
+ps-tree@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014"
+ dependencies:
+ event-stream "~3.3.0"
+
+pseudomap@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
public-encrypt@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
@@ -3605,6 +4474,10 @@ punycode@^1.2.4, punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+q@^1.1.2:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
+
qjobs@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73"
@@ -3621,6 +4494,13 @@ qs@~6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
+query-string@^4.1.0:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.2.tgz#ec0fd765f58a50031a3968c2431386f8947a5cdd"
+ dependencies:
+ object-assign "^4.1.0"
+ strict-uri-encode "^1.0.0"
+
querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -3666,7 +4546,7 @@ raw-loader@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
-rc@~1.1.6:
+rc@^1.0.1, rc@~1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9"
dependencies:
@@ -3675,6 +4555,28 @@ rc@~1.1.6:
minimist "^1.2.0"
strip-json-comments "~1.0.4"
+react-dev-utils@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-0.5.2.tgz#50d0b962d3a94b6c2e8f2011ed6468e4124bc410"
+ dependencies:
+ ansi-html "0.0.5"
+ chalk "1.1.3"
+ escape-string-regexp "1.0.5"
+ filesize "3.3.0"
+ gzip-size "3.0.0"
+ html-entities "1.2.0"
+ opn "4.0.2"
+ recursive-readdir "2.1.1"
+ sockjs-client "1.0.1"
+ strip-ansi "3.0.1"
+
+read-all-stream@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa"
+ dependencies:
+ pinkie-promise "^2.0.0"
+ readable-stream "^2.0.0"
+
read-pkg-up@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
@@ -3690,7 +4592,7 @@ read-pkg@^1.0.0:
normalize-package-data "^2.3.2"
path-type "^1.0.0"
-"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.1.0, readable-stream@^2.2.2:
+readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.1.0, readable-stream@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e"
dependencies:
@@ -3702,7 +4604,7 @@ read-pkg@^1.0.0:
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
-readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@~2.0.0, readable-stream@~2.0.6:
+readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@~2.0.0, readable-stream@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
dependencies:
@@ -3757,6 +4659,26 @@ rechoir@^0.6.2:
dependencies:
resolve "^1.1.6"
+recursive-readdir@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.1.1.tgz#a01cfc7f7f38a53ec096a096f63a50489c3e297c"
+ dependencies:
+ minimatch "3.0.3"
+
+reduce-css-calc@^1.2.6:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
+ dependencies:
+ balanced-match "^0.4.2"
+ math-expression-evaluator "^1.2.14"
+ reduce-function-call "^1.0.1"
+
+reduce-function-call@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99"
+ dependencies:
+ balanced-match "^0.4.2"
+
regenerate@^1.2.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260"
@@ -3780,6 +4702,14 @@ regex-cache@^0.4.2:
is-equal-shallow "^0.1.3"
is-primitive "^2.0.0"
+regexpu-core@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
regexpu-core@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
@@ -3788,6 +4718,12 @@ regexpu-core@^2.0.0:
regjsgen "^0.2.0"
regjsparser "^0.1.4"
+registry-url@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
+ dependencies:
+ rc "^1.0.1"
+
regjsgen@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
@@ -3810,6 +4746,12 @@ repeat-string@^1.5.2:
version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+repeating@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac"
+ dependencies:
+ is-finite "^1.0.0"
+
repeating@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
@@ -3851,6 +4793,10 @@ require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+require-from-string@^1.1.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418"
+
require-main-filename@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
@@ -3915,6 +4861,10 @@ safe-buffer@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
+sax@~1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"
+
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -3923,7 +4873,13 @@ select2@3.5.2-browserify:
version "3.5.2-browserify"
resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.2-browserify.tgz#dc4dafda38d67a734e8a97a46f0d3529ae05391d"
-"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@~5.3.0:
+semver-diff@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
+ dependencies:
+ semver "^5.0.3"
+
+"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.3.0, semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@@ -4000,6 +4956,10 @@ shelljs@^0.7.5:
interpret "^1.0.0"
rechoir "^0.6.2"
+sigmund@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
+
signal-exit@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -4012,6 +4972,10 @@ slice-ansi@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
+slide@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
+
sntp@1.x.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
@@ -4062,6 +5026,17 @@ socket.io@1.7.2:
socket.io-client "1.7.2"
socket.io-parser "2.3.1"
+sockjs-client@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.0.1.tgz#8943ae05b46547bc2054816c409002cf5e2fe026"
+ dependencies:
+ debug "^2.1.0"
+ eventsource "^0.1.3"
+ faye-websocket "~0.7.3"
+ inherits "^2.0.1"
+ json3 "^3.3.2"
+ url-parse "^1.0.1"
+
sockjs-client@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.2.tgz#f0212a8550e4c9468c8cceaeefd2e3493c033ad5"
@@ -4080,14 +5055,20 @@ sockjs@0.3.18:
faye-websocket "^0.10.0"
uuid "^2.0.2"
-source-list-map@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-1.1.1.tgz#1a33ac210ca144d1e561f906ebccab5669ff4cb4"
+sort-keys@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
+ dependencies:
+ is-plain-obj "^1.0.0"
-source-list-map@~0.1.7:
+source-list-map@^0.1.7, source-list-map@~0.1.7:
version "0.1.8"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106"
+source-list-map@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-1.1.1.tgz#1a33ac210ca144d1e561f906ebccab5669ff4cb4"
+
source-map-support@^0.4.2:
version "0.4.11"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.11.tgz#647f939978b38535909530885303daf23279f322"
@@ -4106,7 +5087,7 @@ source-map@^0.4.4:
dependencies:
amdefine ">=0.0.4"
-source-map@^0.5.0, source-map@^0.5.3, source-map@~0.5.1, source-map@~0.5.3:
+source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
@@ -4150,6 +5131,12 @@ spdy@^3.4.1:
select-hose "^2.0.0"
spdy-transport "^2.0.15"
+split@0.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f"
+ dependencies:
+ through "2"
+
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -4184,6 +5171,12 @@ stream-browserify@^2.0.1:
inherits "~2.0.1"
readable-stream "^2.0.2"
+stream-combiner@~0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
+ dependencies:
+ duplexer "~0.1.1"
+
stream-http@^2.3.1:
version "2.6.3"
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.6.3.tgz#4c3ddbf9635968ea2cfd4e48d43de5def2625ac3"
@@ -4194,6 +5187,20 @@ stream-http@^2.3.1:
to-arraybuffer "^1.0.0"
xtend "^4.0.0"
+stream-shift@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
+
+strict-uri-encode@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
+
+string-length@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac"
+ dependencies:
+ strip-ansi "^3.0.0"
+
string-width@^1.0.1, string-width@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -4217,7 +5224,7 @@ stringstream@~0.0.4:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
-strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+strip-ansi@3.0.1, strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
dependencies:
@@ -4249,12 +5256,24 @@ supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
-supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2:
+supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2, supports-color@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
dependencies:
has-flag "^1.0.0"
+svgo@^0.7.0:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
+ dependencies:
+ coa "~1.0.1"
+ colors "~1.1.2"
+ csso "~2.3.1"
+ js-yaml "~3.7.0"
+ mkdirp "~0.5.1"
+ sax "~1.2.1"
+ whet.extend "~0.9.9"
+
table@^3.7.8:
version "3.8.3"
resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
@@ -4325,7 +5344,7 @@ throttleit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
-through@^2.3.6:
+through@2, through@^2.3.6, through@~2.3, through@~2.3.1:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -4333,6 +5352,10 @@ timeago.js@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-2.0.5.tgz#730c74fbdb0b0917a553675a4460e3a7f80db86c"
+timed-out@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a"
+
timers-browserify@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d"
@@ -4363,6 +5386,12 @@ to-fast-properties@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320"
+touch@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/touch/-/touch-1.0.0.tgz#449cbe2dbae5a8c8038e30d71fa0ff464947c4de"
+ dependencies:
+ nopt "~1.0.10"
+
tough-cookie@~2.3.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"
@@ -4435,14 +5464,44 @@ unc-path-regex@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
+undefsafe@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-0.0.3.tgz#ecca3a03e56b9af17385baac812ac83b994a962f"
+
underscore@^1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
+uniq@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+
+uniqid@^4.0.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1"
+ dependencies:
+ macaddress "^0.2.8"
+
+uniqs@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+update-notifier@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.5.0.tgz#07b5dc2066b3627ab3b4f530130f7eddda07a4cc"
+ dependencies:
+ chalk "^1.0.0"
+ configstore "^1.0.0"
+ is-npm "^1.0.0"
+ latest-version "^1.0.0"
+ repeating "^1.1.2"
+ semver-diff "^2.0.0"
+ string-length "^1.0.0"
+
url-parse@1.0.x:
version "1.0.5"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"
@@ -4450,7 +5509,7 @@ url-parse@1.0.x:
querystringify "0.0.x"
requires-port "1.0.x"
-url-parse@^1.1.1:
+url-parse@^1.0.1, url-parse@^1.1.1:
version "1.1.7"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.7.tgz#025cff999653a459ab34232147d89514cc87d74a"
dependencies:
@@ -4491,7 +5550,7 @@ utils-merge@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8"
-uuid@^2.0.2:
+uuid@^2.0.1, uuid@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
@@ -4510,6 +5569,10 @@ vary@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140"
+vendors@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22"
+
verror@1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c"
@@ -4530,13 +5593,52 @@ void-elements@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+vue-hot-reload-api@^2.0.11:
+ version "2.0.11"
+ resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.0.11.tgz#bf26374fb73366ce03f799e65ef5dfd0e28a1568"
+
+vue-loader@^11.3.4:
+ version "11.3.4"
+ resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-11.3.4.tgz#65e10a44ce092d906e14bbc72981dec99eb090d2"
+ dependencies:
+ consolidate "^0.14.0"
+ hash-sum "^1.0.2"
+ js-beautify "^1.6.3"
+ loader-utils "^1.1.0"
+ lru-cache "^4.0.1"
+ postcss "^5.0.21"
+ postcss-load-config "^1.1.0"
+ postcss-selector-parser "^2.0.0"
+ source-map "^0.5.6"
+ vue-hot-reload-api "^2.0.11"
+ vue-style-loader "^2.0.0"
+ vue-template-es2015-compiler "^1.2.2"
+
vue-resource@^0.9.3:
version "0.9.3"
resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d"
-vue@^2.2.4:
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/vue/-/vue-2.2.4.tgz#d0a3a050a80a12356d7950ae5a7b3131048209cc"
+vue-style-loader@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-2.0.5.tgz#f0efac992febe3f12e493e334edb13cd235a3d22"
+ dependencies:
+ hash-sum "^1.0.2"
+ loader-utils "^1.0.2"
+
+vue-template-compiler@^2.2.6:
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.2.6.tgz#2e2928daf0cd0feca9dfc35a9729adeae173ec68"
+ dependencies:
+ de-indent "^1.0.2"
+ he "^1.1.0"
+
+vue-template-es2015-compiler@^1.2.2:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.5.1.tgz#0c36cc57aa3a9ec13e846342cb14a72fcac8bd93"
+
+vue@^2.2.6:
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.2.6.tgz#451714b394dd6d4eae7b773c40c2034a59621aed"
watchpack@^1.3.1:
version "1.3.1"
@@ -4637,7 +5739,7 @@ webpack@^2.3.3:
webpack-sources "^0.2.3"
yargs "^6.0.0"
-websocket-driver@>=0.5.1:
+websocket-driver@>=0.3.6, websocket-driver@>=0.5.1:
version "0.6.5"
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36"
dependencies:
@@ -4647,6 +5749,10 @@ websocket-extensions@>=0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7"
+whet.extend@~0.9.9:
+ version "0.9.9"
+ resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
+
which-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
@@ -4690,6 +5796,14 @@ wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+write-file-atomic@^1.1.2:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.1.tgz#7d45ba32316328dd1ec7d90f60ebc0d845bb759a"
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ slide "^1.1.5"
+
write@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
@@ -4707,6 +5821,12 @@ wtf-8@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a"
+xdg-basedir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2"
+ dependencies:
+ os-homedir "^1.0.0"
+
xmlhttprequest-ssl@1.5.3:
version "1.5.3"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"
@@ -4719,6 +5839,10 @@ y18n@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+yallist@^2.0.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+
yargs-parser@^4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c"