summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc6
-rw-r--r--.gitignore3
-rw-r--r--.gitlab-ci.yml308
-rw-r--r--.gitlab/issue_templates/Bug.md20
-rw-r--r--.gitlab/issue_templates/Feature Proposal.md15
-rw-r--r--.rubocop.yml14
-rw-r--r--CHANGELOG.md286
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile13
-rw-r--r--Gemfile.lock55
-rw-r--r--PROCESS.md13
-rw-r--r--README.md3
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_canceled.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_created.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_failed.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_manual.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_not_found.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_pending.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_running.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_skipped.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_success.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_warning.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_canceled.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_created.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_failed.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_manual.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_not_found.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_pending.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_running.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_skipped.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_success.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_warning.icobin0 -> 4286 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_canceled.icobin5430 -> 0 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_created.icobin5430 -> 0 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_failed.icobin5430 -> 0 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_manual.icobin5430 -> 0 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_not_found.icobin5430 -> 0 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_pending.icobin5430 -> 0 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_running.icobin5430 -> 0 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_skipped.icobin5430 -> 0 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_success.icobin5430 -> 0 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_warning.icobin5430 -> 0 bytes
-rw-r--r--app/assets/javascripts/awards_handler.js3
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js2
-rw-r--r--app/assets/javascripts/blob/blob_fork_suggestion.js63
-rw-r--r--app/assets/javascripts/blob/notebook/index.js6
-rw-r--r--app/assets/javascripts/blob/pdf/index.js12
-rw-r--r--app/assets/javascripts/blob/viewer/index.js120
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js3
-rw-r--r--app/assets/javascripts/boards/components/board_list.js9
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js16
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js3
-rw-r--r--app/assets/javascripts/boards/models/list.js11
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js3
-rw-r--r--app/assets/javascripts/build.js24
-rw-r--r--app/assets/javascripts/ci_status_icons.js34
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js42
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-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.js46
-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.js14
-rw-r--r--app/assets/javascripts/due_date_select.js11
-rw-r--r--app/assets/javascripts/environments/components/environment.vue (renamed from app/assets/javascripts/environments/components/environment.js)155
-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/environments_bundle.js21
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js21
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue (renamed from app/assets/javascripts/environments/folder/environments_folder_view.js)106
-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.js798
-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/filtered_search/filtered_search_visual_tokens.js14
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js11
-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_show/issue_title.vue23
-rw-r--r--app/assets/javascripts/labels_select.js8
-rw-r--r--app/assets/javascripts/landing.js37
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js15
-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/regexp.js10
-rw-r--r--app/assets/javascripts/line_highlighter.js21
-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_tabs.js12
-rw-r--r--app/assets/javascripts/merge_request_widget.js8
-rw-r--r--app/assets/javascripts/milestone_select.js3
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js7
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js61
-rw-r--r--app/assets/javascripts/notebook/cells/code.vue58
-rw-r--r--app/assets/javascripts/notebook/cells/code/index.vue57
-rw-r--r--app/assets/javascripts/notebook/cells/index.js2
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue98
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue22
-rw-r--r--app/assets/javascripts/notebook/cells/output/image.vue27
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue83
-rw-r--r--app/assets/javascripts/notebook/cells/prompt.vue30
-rw-r--r--app/assets/javascripts/notebook/index.vue75
-rw-r--r--app/assets/javascripts/notebook/lib/highlight.js22
-rw-r--r--app/assets/javascripts/notes.js22
-rw-r--r--app/assets/javascripts/pdf/assets/img/bg.gifbin0 -> 58 bytes
-rw-r--r--app/assets/javascripts/pdf/index.vue73
-rw-r--r--app/assets/javascripts/pdf/page/index.vue68
-rw-r--r--app/assets/javascripts/pipelines/components/async_button.vue (renamed from app/assets/javascripts/vue_pipelines_index/components/async_button.vue)14
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue (renamed from app/assets/javascripts/vue_pipelines_index/components/empty_state.vue)2
-rw-r--r--app/assets/javascripts/pipelines/components/error_state.vue (renamed from app/assets/javascripts/vue_pipelines_index/components/error_state.vue)0
-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)75
-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.js98
-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)10
-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.js30
-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_tabs.js22
-rw-r--r--app/assets/javascripts/users_select.js4
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/time_ago.js71
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js61
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js45
-rw-r--r--app/assets/stylesheets/framework/animations.scss14
-rw-r--r--app/assets/stylesheets/framework/awards.scss7
-rw-r--r--app/assets/stylesheets/framework/blocks.scss58
-rw-r--r--app/assets/stylesheets/framework/common.scss11
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss74
-rw-r--r--app/assets/stylesheets/framework/files.scss14
-rw-r--r--app/assets/stylesheets/framework/filters.scss22
-rw-r--r--app/assets/stylesheets/framework/header.scss9
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss7
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/timeline.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss69
-rw-r--r--app/assets/stylesheets/framework/variables.scss11
-rw-r--r--app/assets/stylesheets/pages/builds.scss14
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss44
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss6
-rw-r--r--app/assets/stylesheets/pages/groups.scss23
-rw-r--r--app/assets/stylesheets/pages/issuable.scss10
-rw-r--r--app/assets/stylesheets/pages/issues.scss13
-rw-r--r--app/assets/stylesheets/pages/note_form.scss3
-rw-r--r--app/assets/stylesheets/pages/notes.scss17
-rw-r--r--app/assets/stylesheets/pages/profile.scss59
-rw-r--r--app/assets/stylesheets/pages/projects.scss5
-rw-r--r--app/assets/stylesheets/pages/search.scss15
-rw-r--r--app/assets/stylesheets/pages/tree.scss1
-rw-r--r--app/assets/stylesheets/pages/wiki.scss6
-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.rb10
-rw-r--r--app/controllers/admin/hooks_controller.rb26
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/concerns/creates_commit.rb62
-rw-r--r--app/controllers/concerns/markdown_preview.rb19
-rw-r--r--app/controllers/concerns/membership_actions.rb42
-rw-r--r--app/controllers/concerns/renders_blob.rb21
-rw-r--r--app/controllers/concerns/service_params.rb1
-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.rb37
-rw-r--r--app/controllers/projects/builds_controller.rb15
-rw-r--r--app/controllers/projects/commit_controller.rb10
-rw-r--r--app/controllers/projects/git_http_controller.rb6
-rw-r--r--app/controllers/projects/hooks_controller.rb13
-rw-r--r--app/controllers/projects/milestones_controller.rb1
-rw-r--r--app/controllers/projects/project_members_controller.rb24
-rw-r--r--app/controllers/projects/raw_controller.rb2
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb23
-rw-r--r--app/controllers/projects/tree_controller.rb6
-rw-r--r--app/controllers/projects/wikis_controller.rb21
-rw-r--r--app/controllers/projects_controller.rb19
-rw-r--r--app/controllers/sessions_controller.rb5
-rw-r--r--app/controllers/snippets_controller.rb18
-rw-r--r--app/controllers/unicorn_test_controller.rb12
-rw-r--r--app/finders/merge_requests_finder.rb2
-rw-r--r--app/helpers/application_helper.rb32
-rw-r--r--app/helpers/blob_helper.rb114
-rw-r--r--app/helpers/ci_status_helper.rb25
-rw-r--r--app/helpers/events_helper.rb5
-rw-r--r--app/helpers/icons_helper.rb5
-rw-r--r--app/helpers/issuables_helper.rb11
-rw-r--r--app/helpers/markup_helper.rb (renamed from app/helpers/gitlab_markdown_helper.rb)148
-rw-r--r--app/helpers/merge_requests_helper.rb15
-rw-r--r--app/helpers/projects_helper.rb11
-rw-r--r--app/helpers/services_helper.rb4
-rw-r--r--app/helpers/sorting_helper.rb8
-rw-r--r--app/helpers/submodule_helper.rb12
-rw-r--r--app/helpers/todos_helper.rb12
-rw-r--r--app/helpers/tree_helper.rb6
-rw-r--r--app/mailers/base_mailer.rb2
-rw-r--r--app/models/application_setting.rb12
-rw-r--r--app/models/blob.rb177
-rw-r--r--app/models/blob_viewer/base.rb96
-rw-r--r--app/models/blob_viewer/binary_stl.rb10
-rw-r--r--app/models/blob_viewer/client_side.rb11
-rw-r--r--app/models/blob_viewer/download.rb17
-rw-r--r--app/models/blob_viewer/empty.rb9
-rw-r--r--app/models/blob_viewer/image.rb12
-rw-r--r--app/models/blob_viewer/markup.rb10
-rw-r--r--app/models/blob_viewer/notebook.rb12
-rw-r--r--app/models/blob_viewer/pdf.rb12
-rw-r--r--app/models/blob_viewer/rich.rb11
-rw-r--r--app/models/blob_viewer/server_side.rb11
-rw-r--r--app/models/blob_viewer/simple.rb11
-rw-r--r--app/models/blob_viewer/sketch.rb12
-rw-r--r--app/models/blob_viewer/svg.rb12
-rw-r--r--app/models/blob_viewer/text.rb11
-rw-r--r--app/models/blob_viewer/text_stl.rb5
-rw-r--r--app/models/blob_viewer/video.rb12
-rw-r--r--app/models/ci/pipeline.rb20
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/concerns/cache_markdown_field.rb128
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/routable.rb15
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/identity.rb2
-rw-r--r--app/models/individual_note_discussion.rb4
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/label.rb1
-rw-r--r--app/models/member.rb33
-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.rb24
-rw-r--r--app/models/merge_request_diff.rb2
-rw-r--r--app/models/network/graph.rb3
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/out_of_context_discussion.rb6
-rw-r--r--app/models/project.rb12
-rw-r--r--app/models/project_team.rb4
-rw-r--r--app/models/repository.rb40
-rw-r--r--app/models/service.rb3
-rw-r--r--app/models/snippet.rb26
-rw-r--r--app/models/snippet_blob.rb59
-rw-r--r--app/models/todo.rb12
-rw-r--r--app/models/user.rb19
-rw-r--r--app/policies/project_policy.rb44
-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.rb9
-rw-r--r--app/services/boards/issues/move_service.rb2
-rw-r--r--app/services/ci/expire_pipeline_cache_service.rb51
-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/merge_requests/build_service.rb2
-rw-r--r--app/services/projects/create_service.rb3
-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/slash_commands/interpret_service.rb22
-rw-r--r--app/services/users/activity_service.rb22
-rw-r--r--app/services/users/build_service.rb29
-rw-r--r--app/services/users/create_service.rb4
-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.haml15
-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/dashboard/index.html.haml6
-rw-r--r--app/views/admin/hooks/_form.html.haml40
-rw-r--r--app/views/admin/hooks/edit.html.haml14
-rw-r--r--app/views/admin/hooks/index.html.haml55
-rw-r--r--app/views/dashboard/_groups_head.html.haml6
-rw-r--r--app/views/dashboard/todos/_todo.html.haml6
-rw-r--r--app/views/events/_event.atom.builder1
-rw-r--r--app/views/explore/groups/index.html.haml9
-rw-r--r--app/views/help/_shortcuts.html.haml8
-rw-r--r--app/views/help/index.html.haml3
-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/_project.html.haml6
-rw-r--r--app/views/notify/repository_push_email.html.haml2
-rw-r--r--app/views/projects/_fork_suggestion.html.haml11
-rw-r--r--app/views/projects/_last_commit.html.haml3
-rw-r--r--app/views/projects/_readme.html.haml3
-rw-r--r--app/views/projects/_wiki.html.haml3
-rw-r--r--app/views/projects/blob/_blob.html.haml7
-rw-r--r--app/views/projects/blob/_content.html.haml8
-rw-r--r--app/views/projects/blob/_download.html.haml7
-rw-r--r--app/views/projects/blob/_header.html.haml21
-rw-r--r--app/views/projects/blob/_image.html.haml2
-rw-r--r--app/views/projects/blob/_markup.html.haml2
-rw-r--r--app/views/projects/blob/_render_error.html.haml7
-rw-r--r--app/views/projects/blob/_svg.html.haml9
-rw-r--r--app/views/projects/blob/_text.html.haml2
-rw-r--r--app/views/projects/blob/_too_large.html.haml5
-rw-r--r--app/views/projects/blob/_viewer.html.haml14
-rw-r--r--app/views/projects/blob/_viewer_switcher.html.haml12
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/preview.html.haml8
-rw-r--r--app/views/projects/blob/show.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_download.html.haml7
-rw-r--r--app/views/projects/blob/viewers/_empty.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_image.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_markup.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_notebook.html.haml (renamed from app/views/projects/blob/_notebook.html.haml)2
-rw-r--r--app/views/projects/blob/viewers/_pdf.html.haml (renamed from app/views/projects/blob/_pdf.html.haml)2
-rw-r--r--app/views/projects/blob/viewers/_sketch.html.haml (renamed from app/views/projects/blob/_sketch.html.haml)2
-rw-r--r--app/views/projects/blob/viewers/_stl.html.haml (renamed from app/views/projects/blob/_stl.html.haml)2
-rw-r--r--app/views/projects/blob/viewers/_svg.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_text.html.haml1
-rw-r--r--app/views/projects/blob/viewers/_video.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml16
-rw-r--r--app/views/projects/builds/show.html.haml10
-rw-r--r--app/views/projects/diffs/_content.html.haml4
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/diffs/_file.html.haml4
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/environments/metrics.html.haml3
-rw-r--r--app/views/projects/hooks/_index.html.haml24
-rw-r--r--app/views/projects/hooks/edit.html.haml14
-rw-r--r--app/views/projects/issues/show.html.haml5
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_pipelines.html.haml3
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_error.html.haml6
-rw-r--r--app/views/projects/milestones/edit.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml7
-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.haml3
-rw-r--r--app/views/projects/pipelines/index.html.haml2
-rw-r--r--app/views/projects/services/edit.html.haml1
-rw-r--r--app/views/projects/settings/_head.html.haml2
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml1
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml3
-rw-r--r--app/views/projects/tags/show.html.haml3
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml4
-rw-r--r--app/views/projects/tree/show.html.haml10
-rw-r--r--app/views/projects/triggers/_form.html.haml2
-rw-r--r--app/views/projects/wikis/_main_links.html.haml2
-rw-r--r--app/views/projects/wikis/show.html.haml3
-rw-r--r--app/views/search/results/_issue.html.haml3
-rw-r--r--app/views/search/results/_merge_request.html.haml3
-rw-r--r--app/views/search/results/_milestone.html.haml3
-rw-r--r--app/views/search/results/_note.html.haml3
-rw-r--r--app/views/search/results/_snippet_blob.html.haml4
-rw-r--r--app/views/shared/_branch_switcher.html.haml6
-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/_service_settings.html.haml3
-rw-r--r--app/views/shared/_user_callout.html.haml17
-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_explore_groups_splash.svg1
-rw-r--r--app/views/shared/icons/_mr_bold.svg3
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-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/milestones/_issuable.html.haml13
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml6
-rw-r--r--app/views/shared/projects/_list.html.haml1
-rw-r--r--app/views/shared/projects/_project.html.haml43
-rw-r--r--app/views/shared/snippets/_blob.html.haml29
-rw-r--r--app/views/shared/web_hooks/_form.html.haml182
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/views/users/show.html.haml10
-rw-r--r--app/workers/clear_database_cache_worker.rb24
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb2
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb57
-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/12910-personal-snippet-prep-2.yml4
-rw-r--r--changelogs/unreleased/1440-db-backup-ssl-support.yml4
-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/19364-webhook-edit.yml4
-rw-r--r--changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml4
-rw-r--r--changelogs/unreleased/20378-natural-sort-issue-numbers.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/2120-issues-search-bar-not-picking-up.yml4
-rw-r--r--changelogs/unreleased/21451-allow-disable-mr-link.yml4
-rw-r--r--changelogs/unreleased/21683-show-created-group-name-flash.yml4
-rw-r--r--changelogs/unreleased/22303-symbolic-in-tree.yml4
-rw-r--r--changelogs/unreleased/22826-ui-inconsistency-different-files-views-find-file-button-missing.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/26437-closed-by.yml4
-rw-r--r--changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml4
-rw-r--r--changelogs/unreleased/26488-target-disabled-mr.yml4
-rw-r--r--changelogs/unreleased/26509-show-update-time.yml4
-rw-r--r--changelogs/unreleased/26585-remove-readme-view-caching.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/27376-bvl-load-pipelinestatus-in-batch.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/27827-cleanup-markdown.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/28020-improve-todo-list-when-comes-from-yourself.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/28202_decrease_abc_threshold_step1.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/28457-slash-command-board-move.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/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/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml4
-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/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/29181-add-more-tests-for-spec-controllers-projects-builds-controller-spec-rb.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/29505-allow-admins-sudo-to-blocked-users.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/29712-unnecessary-wait-for-ajax.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/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/29903-remove-user-is-admin-flag-from-api.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/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/30305-oauth-token-push-code.yml4
-rw-r--r--changelogs/unreleased/30400-fix-blob-highlighting-in-search.yml4
-rw-r--r--changelogs/unreleased/30457-expire-note-destroy.yml4
-rw-r--r--changelogs/unreleased/30466-click-x-to-remove-filter.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/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml4
-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/30779-show-mr-subnav-issue-tracker.yml4
-rw-r--r--changelogs/unreleased/31009-disable-test-settings-on-services-when-repository-is-empty.yml4
-rw-r--r--changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml4
-rw-r--r--changelogs/unreleased/31193-ff-copy.yml4
-rw-r--r--changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml4
-rw-r--r--changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step1.yml4
-rw-r--r--changelogs/unreleased/4195-add-sorting-to-project-milestones.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-aria-to-icon.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-tanuki-ci-status-favicons.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-username-to-activity-feed.yml4
-rw-r--r--changelogs/unreleased/add_index_on_ci_builds_user_id.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/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/diff-discussion-buttons-spacing.yml4
-rw-r--r--changelogs/unreleased/dm-blob-download-button.yml4
-rw-r--r--changelogs/unreleased/dm-blob-viewers.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-fix-position-tracer-for-hidden-lines.yml5
-rw-r--r--changelogs/unreleased/dm-sidekiq-5.yml4
-rw-r--r--changelogs/unreleased/dm-snippet-blob-viewers.yml4
-rw-r--r--changelogs/unreleased/dm-video-viewer.yml4
-rw-r--r--changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml5
-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-fix-project-view.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/emoji-menu-duplicated-search-icon.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-missing-capitalisation-buttons.yml4
-rw-r--r--changelogs/unreleased/fix-notify-post-receive.yml4
-rw-r--r--changelogs/unreleased/fix-preloading-merge_request_diff.yml4
-rw-r--r--changelogs/unreleased/fix-project-visibility-setting.yml4
-rw-r--r--changelogs/unreleased/fix-trace-encoding.yml4
-rw-r--r--changelogs/unreleased/fix-trace-seeking.yml4
-rw-r--r--changelogs/unreleased/fix-user-profile-tabs-showing-raw-json-instead.yml5
-rw-r--r--changelogs/unreleased/fix-web_hooks-index.yml4
-rw-r--r--changelogs/unreleased/fix_admin_monitoring_background.yml4
-rw-r--r--changelogs/unreleased/fix_build_header_line_height.yml4
-rw-r--r--changelogs/unreleased/fix_emoji_parser.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_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/gl-version-backup-file.yml4
-rw-r--r--changelogs/unreleased/group-assignee-dropdown-send-group-id.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/issues-empty-state-not-centered.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_markdown_tables_thinner.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/metrics-graph-error-fix.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/more-mr-filters.yml4
-rw-r--r--changelogs/unreleased/mr-diff-size-overflow.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/mr-widget-bug-fix.yml5
-rw-r--r--changelogs/unreleased/mrchrisw-22740-merge-api.yml4
-rw-r--r--changelogs/unreleased/namespace-race-condition.yml4
-rw-r--r--changelogs/unreleased/new-resolvable-discussion.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/pms-lighter-colors.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/related-branch-ci-status-icon-alignment.yml4
-rw-r--r--changelogs/unreleased/remember-me-missasligned-mobile.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/replace_header_mr_icon.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-bump-sidekiq-version.yml4
-rw-r--r--changelogs/unreleased/sh-optimize-duplicate-routable-full-path.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/siemens-gitlab-ce-fix-subgroup-hide-button.yml4
-rw-r--r--changelogs/unreleased/simplify-docs-trigger.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-make-user-master-project-by-admin.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/user-callout-showing-on-all-profiles.yml4
-rw-r--r--changelogs/unreleased/user-profile-join-date.yml4
-rw-r--r--changelogs/unreleased/zj-api-fix-build-events.yml4
-rw-r--r--changelogs/unreleased/zj-chat-notification-default-branch.yml4
-rw-r--r--changelogs/unreleased/zj-dockerfiles.yml4
-rw-r--r--changelogs/unreleased/zj-fk-ci-triggers.yml4
-rw-r--r--changelogs/unreleased/zj-kube-service-auto-fill.yml4
-rw-r--r--config/database.yml.mysql2
-rw-r--r--config/database.yml.postgresql5
-rw-r--r--config/gitlab.yml.example5
-rw-r--r--config/initializers/1_settings.rb36
-rw-r--r--config/initializers/carrierwave.rb2
-rw-r--r--config/initializers/rspec_profiling.rb8
-rw-r--r--config/routes.rb2
-rw-r--r--config/routes/admin.rb9
-rw-r--r--config/routes/project.rb4
-rw-r--r--config/routes/snippets.rb1
-rw-r--r--config/routes/test.rb2
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--config/webpack.config.js23
-rw-r--r--db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb1
-rw-r--r--db/migrate/20160608195742_add_repository_storage_to_projects.rb1
-rw-r--r--db/migrate/20160713222618_add_usage_ping_to_application_settings.rb9
-rw-r--r--db/migrate/20160715154212_add_request_access_enabled_to_projects.rb1
-rw-r--r--db/migrate/20160715204316_add_request_access_enabled_to_groups.rb1
-rw-r--r--db/migrate/20160831223750_remove_features_enabled_from_projects.rb1
-rw-r--r--db/migrate/20160913162434_remove_projects_pushes_since_gc.rb1
-rw-r--r--db/migrate/20161007073613_create_user_activities.rb7
-rw-r--r--db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb1
-rw-r--r--db/migrate/20170124193205_add_two_factor_columns_to_users.rb1
-rw-r--r--db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb1
-rw-r--r--db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb1
-rw-r--r--db/migrate/20170307125949_add_last_activity_on_to_users.rb9
-rw-r--r--db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb1
-rw-r--r--db/migrate/20170328010804_add_uuid_to_application_settings.rb16
-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/migrate/20170421102337_remove_nil_type_services.rb12
-rw-r--r--db/migrate/20170423064036_add_index_on_ci_builds_updated_at.rb19
-rw-r--r--db/migrate/20170424095707_add_index_on_ci_builds_user_id.rb19
-rw-r--r--db/migrate/20170424142900_add_index_to_web_hooks_type.rb15
-rw-r--r--db/migrate/20170426175636_fill_missing_uuid_on_application_settings.rb10
-rw-r--r--db/migrate/20170426181740_add_index_on_ci_runners_contacted_at.rb19
-rw-r--r--db/post_migrate/20161128170531_drop_user_activities_table.rb9
-rw-r--r--db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb87
-rw-r--r--db/schema.rb24
-rw-r--r--doc/README.md257
-rw-r--r--doc/administration/auth/ldap.md6
-rw-r--r--doc/administration/gitaly/index.md2
-rw-r--r--doc/administration/high_availability/load_balancer.md3
-rw-r--r--doc/administration/high_availability/nfs.md19
-rw-r--r--doc/administration/high_availability/redis.md2
-rw-r--r--doc/administration/integration/plantuml.md6
-rw-r--r--doc/administration/integration/terminal.md4
-rw-r--r--doc/api/deployments.md3
-rw-r--r--doc/api/issues.md61
-rw-r--r--doc/api/jobs.md7
-rw-r--r--doc/api/keys.md1
-rw-r--r--doc/api/merge_requests.md16
-rw-r--r--doc/api/projects.md15
-rw-r--r--doc/api/services.md79
-rw-r--r--doc/api/users.md74
-rw-r--r--doc/articles/index.md16
-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/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/README.md1
-rw-r--r--doc/development/ci_setup.md47
-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/style_guide_js.md50
-rw-r--r--doc/development/fe_guide/testing.md11
-rw-r--r--doc/development/fe_guide/vue.md355
-rw-r--r--doc/development/testing.md30
-rw-r--r--doc/development/writing_documentation.md28
-rw-r--r--doc/gitlab-basics/create-project.md36
-rw-r--r--doc/gitlab-basics/img/create_new_project_button.pngbin6978 -> 3702 bytes
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/integration/chat_commands.md18
-rw-r--r--doc/intro/README.md2
-rw-r--r--doc/migrate_ci_to_ce/README.md58
-rw-r--r--doc/raketasks/backup_restore.md252
-rw-r--r--doc/topics/git/index.md4
-rw-r--r--doc/university/glossary/README.md2
-rw-r--r--doc/update/8.10-to-8.11.md2
-rw-r--r--doc/update/8.11-to-8.12.md2
-rw-r--r--doc/update/8.12-to-8.13.md2
-rw-r--r--doc/update/8.13-to-8.14.md2
-rw-r--r--doc/update/9.0-to-9.1.md11
-rw-r--r--doc/user/admin_area/img/cohorts.pngbin0 -> 439635 bytes
-rw-r--r--doc/user/admin_area/monitoring/health_check.md76
-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/markdown.md18
-rw-r--r--doc/user/permissions.md3
-rw-r--r--doc/user/profile/account/delete_account.md2
-rw-r--r--doc/user/project/integrations/bamboo.md10
-rw-r--r--doc/user/project/integrations/kubernetes.md8
-rw-r--r--doc/user/project/integrations/microsoft_teams.md6
-rw-r--r--doc/user/project/integrations/project_services.md5
-rw-r--r--doc/user/project/integrations/slack.md59
-rw-r--r--doc/user/project/integrations/slack_slash_commands.md25
-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/project/slash_commands.md1
-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/groups.md2
-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--features/group/members.feature34
-rw-r--r--features/profile/profile.feature4
-rw-r--r--features/project/forked_merge_requests.feature3
-rw-r--r--features/project/issues/issues.feature6
-rw-r--r--features/project/merge_requests/revert.feature2
-rw-r--r--features/project/snippets.feature1
-rw-r--r--features/project/source/browse_files.feature9
-rw-r--r--features/project/source/markdown_render.feature12
-rw-r--r--features/project/team_management.feature20
-rw-r--r--features/snippets/snippets.feature1
-rw-r--r--features/steps/group/members.rb69
-rw-r--r--features/steps/project/commits/commits.rb4
-rw-r--r--features/steps/project/forked_merge_requests.rb2
-rw-r--r--features/steps/project/issues/issues.rb11
-rw-r--r--features/steps/project/merge_requests.rb3
-rw-r--r--features/steps/project/merge_requests/acceptance.rb2
-rw-r--r--features/steps/project/merge_requests/revert.rb2
-rw-r--r--features/steps/project/snippets.rb5
-rw-r--r--features/steps/project/source/browse_files.rb18
-rw-r--r--features/steps/project/source/markdown_render.rb12
-rw-r--r--features/steps/project/team_management.rb77
-rw-r--r--features/steps/shared/active_tab.rb5
-rw-r--r--features/steps/shared/authentication.rb51
-rw-r--r--features/steps/shared/markdown.rb2
-rw-r--r--features/steps/snippets/snippets.rb4
-rw-r--r--features/support/login_helpers.rb19
-rw-r--r--lib/api/commits.rb4
-rw-r--r--lib/api/entities.rb11
-rw-r--r--lib/api/files.rb4
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/helpers/internal_helpers.rb6
-rw-r--r--lib/api/internal.rb4
-rw-r--r--lib/api/issues.rb17
-rw-r--r--lib/api/merge_requests.rb34
-rw-r--r--lib/api/projects.rb7
-rw-r--r--lib/api/session.rb4
-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/api/v3/merge_requests.rb2
-rw-r--r--lib/backup/database.rb26
-rw-r--r--lib/backup/manager.rb37
-rw-r--r--lib/banzai/filter/emoji_filter.rb5
-rw-r--r--lib/banzai/filter/issuable_state_filter.rb6
-rw-r--r--lib/banzai/filter/plantuml_filter.rb8
-rw-r--r--lib/banzai/issuable_extractor.rb6
-rw-r--r--lib/banzai/renderer.rb41
-rw-r--r--lib/ci/ansi2html.rb2
-rw-r--r--lib/container_registry/path.rb10
-rw-r--r--lib/github/client.rb23
-rw-r--r--lib/github/collection.rb29
-rw-r--r--lib/github/error.rb3
-rw-r--r--lib/github/import.rb409
-rw-r--r--lib/github/rate_limit.rb27
-rw-r--r--lib/github/repositories.rb19
-rw-r--r--lib/github/representation/base.rb30
-rw-r--r--lib/github/representation/branch.rb51
-rw-r--r--lib/github/representation/comment.rb42
-rw-r--r--lib/github/representation/issuable.rb37
-rw-r--r--lib/github/representation/issue.rb25
-rw-r--r--lib/github/representation/label.rb13
-rw-r--r--lib/github/representation/milestone.rb25
-rw-r--r--lib/github/representation/pull_request.rb78
-rw-r--r--lib/github/representation/release.rb17
-rw-r--r--lib/github/representation/repo.rb6
-rw-r--r--lib/github/representation/user.rb15
-rw-r--r--lib/github/response.rb25
-rw-r--r--lib/github/user.rb24
-rw-r--r--lib/gitlab/asciidoc.rb20
-rw-r--r--lib/gitlab/auth.rb2
-rw-r--r--lib/gitlab/cache/ci/project_pipeline_status.rb53
-rw-r--r--lib/gitlab/ci/trace/stream.rb15
-rw-r--r--lib/gitlab/data_builder/push.rb9
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb4
-rw-r--r--lib/gitlab/diff/position_tracer.rb9
-rw-r--r--lib/gitlab/email/handler/base_handler.rb4
-rw-r--r--lib/gitlab/email/handler/create_issue_handler.rb5
-rw-r--r--lib/gitlab/email/handler/create_note_handler.rb10
-rw-r--r--lib/gitlab/email/handler/unsubscribe_handler.rb6
-rw-r--r--lib/gitlab/email/receiver.rb5
-rw-r--r--lib/gitlab/git/blob.rb4
-rw-r--r--lib/gitlab/git/encoding_helper.rb8
-rw-r--r--lib/gitlab/git/index.rb49
-rw-r--r--lib/gitlab/git/repository.rb58
-rw-r--r--lib/gitlab/gitaly_client.rb2
-rw-r--r--lib/gitlab/gitaly_client/commit.rb29
-rw-r--r--lib/gitlab/gitaly_client/ref.rb8
-rw-r--r--lib/gitlab/google_code_import/importer.rb70
-rw-r--r--lib/gitlab/issuable_sorter.rb29
-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/other_markup.rb10
-rw-r--r--lib/gitlab/regex.rb16
-rw-r--r--lib/gitlab/template/dockerfile_template.rb4
-rw-r--r--lib/gitlab/usage_data.rb65
-rw-r--r--lib/gitlab/user_access.rb4
-rw-r--r--lib/gitlab/user_activities.rb34
-rw-r--r--lib/gitlab/workhorse.rb2
-rw-r--r--lib/tasks/brakeman.rake2
-rw-r--r--lib/tasks/cache.rake7
-rw-r--r--lib/tasks/gitlab/gitaly.rake37
-rw-r--r--lib/tasks/gitlab/update_templates.rake8
-rw-r--r--lib/tasks/import.rake112
-rw-r--r--package.json5
-rw-r--r--rubocop/cop/migration/add_column_with_default_to_large_table.rb51
-rw-r--r--rubocop/cop/migration/reversible_add_column_with_default.rb (renamed from rubocop/cop/migration/add_column_with_default.rb)21
-rw-r--r--rubocop/rubocop.rb3
-rwxr-xr-xscripts/lint-doc.sh1
-rw-r--r--[-rwxr-xr-x]scripts/prepare_build.sh60
-rwxr-xr-xscripts/static-analysis40
-rw-r--r--scripts/utils.sh14
-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/blob_controller_spec.rb67
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb2
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb55
-rw-r--r--spec/controllers/profiles/personal_access_tokens_controller_spec.rb (renamed from spec/controllers/profiles/personal_access_tokens_spec.rb)0
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb55
-rw-r--r--spec/controllers/projects/builds_controller_spec.rb406
-rw-r--r--spec/controllers/projects/builds_controller_specs.rb47
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb2
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb4
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb15
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb4
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb4
-rw-r--r--spec/controllers/projects/services_controller_spec.rb49
-rw-r--r--spec/controllers/projects/todos_controller_spec.rb (renamed from spec/controllers/projects/todo_controller_spec.rb)2
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb10
-rw-r--r--spec/controllers/projects/wikis_controller_spec.rb16
-rw-r--r--spec/controllers/projects_controller_spec.rb10
-rw-r--r--spec/controllers/sessions_controller_spec.rb10
-rw-r--r--spec/controllers/snippets_controller_spec.rb12
-rw-r--r--spec/factories/ci/builds.rb13
-rw-r--r--spec/factories/issues.rb4
-rw-r--r--spec/factories/merge_requests.rb4
-rw-r--r--spec/factories/merge_requests_closing_issues.rb6
-rw-r--r--spec/factories/project_hooks.rb2
-rw-r--r--spec/factories/projects.rb4
-rw-r--r--spec/factories/services.rb13
-rw-r--r--spec/features/admin/admin_cohorts_spec.rb15
-rw-r--r--spec/features/admin/admin_hooks_spec.rb43
-rw-r--r--spec/features/admin/admin_labels_spec.rb2
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/admin/admin_requests_profiles_spec.rb69
-rw-r--r--spec/features/admin/admin_users_spec.rb2
-rw-r--r--spec/features/atom/users_spec.rb2
-rw-r--r--spec/features/auto_deploy_spec.rb4
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb1
-rw-r--r--spec/features/boards/boards_spec.rb1
-rw-r--r--spec/features/boards/new_issue_spec.rb1
-rw-r--r--spec/features/boards/sidebar_spec.rb1
-rw-r--r--spec/features/calendar_spec.rb4
-rw-r--r--spec/features/commits_spec.rb2
-rw-r--r--spec/features/copy_as_gfm_spec.rb6
-rw-r--r--spec/features/cycle_analytics_spec.rb4
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb2
-rw-r--r--spec/features/dashboard/groups_list_spec.rb2
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb2
-rw-r--r--spec/features/dashboard_issues_spec.rb2
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb4
-rw-r--r--spec/features/explore/groups_list_spec.rb30
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb28
-rw-r--r--spec/features/global_search_spec.rb2
-rw-r--r--spec/features/groups/issues_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/issuables/issuable_list_spec.rb2
-rw-r--r--spec/features/issues/award_emoji_spec.rb1
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb2
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb (renamed from spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb)6
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb3
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb1
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb28
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb15
-rw-r--r--spec/features/issues/note_polling_spec.rb14
-rw-r--r--spec/features/issues/update_issues_spec.rb2
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb1
-rw-r--r--spec/features/issues_spec.rb24
-rw-r--r--spec/features/markdown_spec.rb2
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb2
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb10
-rw-r--r--spec/features/merge_requests/deleted_source_branch_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb2
-rw-r--r--spec/features/merge_requests/diffs_spec.rb47
-rw-r--r--spec/features/merge_requests/filter_by_labels_spec.rb1
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb1
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb2
-rw-r--r--spec/features/merge_requests/pipelines_spec.rb2
-rw-r--r--spec/features/merge_requests/reset_filters_spec.rb1
-rw-r--r--spec/features/merge_requests/update_merge_requests_spec.rb2
-rw-r--r--spec/features/merge_requests/user_posts_notes_spec.rb (renamed from spec/features/merge_requests/user_posts_notes.rb)0
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb1
-rw-r--r--spec/features/merge_requests/versions_spec.rb9
-rw-r--r--spec/features/merge_requests/widget_deployments_spec.rb2
-rw-r--r--spec/features/merge_requests/widget_spec.rb23
-rw-r--r--spec/features/milestone_spec.rb2
-rw-r--r--spec/features/milestones/milestones_spec.rb1
-rw-r--r--spec/features/participants_autocomplete_spec.rb4
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb336
-rw-r--r--spec/features/projects/blobs/edit_spec.rb1
-rw-r--r--spec/features/projects/blobs/user_create_spec.rb5
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb7
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb2
-rw-r--r--spec/features/projects/edit_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb2
-rw-r--r--spec/features/projects/features_visibility_spec.rb3
-rw-r--r--spec/features/projects/files/browse_files_spec.rb5
-rw-r--r--spec/features/projects/files/creating_a_file_spec.rb8
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb8
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb4
-rw-r--r--spec/features/projects/files/files_sort_submodules_with_folders_spec.rb2
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb2
-rw-r--r--spec/features/projects/files/find_files_spec.rb30
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb2
-rw-r--r--spec/features/projects/files/undo_template_spec.rb15
-rw-r--r--spec/features/projects/issuable_templates_spec.rb12
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb1
-rw-r--r--spec/features/projects/members/group_links_spec.rb2
-rw-r--r--spec/features/projects/members/list_spec.rb90
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb1
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb11
-rw-r--r--spec/features/projects/merge_request_button_spec.rb4
-rw-r--r--spec/features/projects/milestones/milestone_spec.rb23
-rw-r--r--spec/features/projects/ref_switcher_spec.rb1
-rw-r--r--spec/features/projects/settings/integration_settings_spec.rb94
-rw-r--r--spec/features/projects/snippets/show_spec.rb132
-rw-r--r--spec/features/projects/user_create_dir_spec.rb1
-rw-r--r--spec/features/projects/view_on_env_spec.rb6
-rw-r--r--spec/features/projects/wiki/shortcuts_spec.rb20
-rw-r--r--spec/features/projects_spec.rb14
-rw-r--r--spec/features/protected_branches/access_control_ce_spec.rb2
-rw-r--r--spec/features/protected_branches_spec.rb6
-rw-r--r--spec/features/protected_tags_spec.rb4
-rw-r--r--spec/features/search_spec.rb8
-rw-r--r--spec/features/security/project/internal_access_spec.rb38
-rw-r--r--spec/features/security/project/private_access_spec.rb32
-rw-r--r--spec/features/security/project/public_access_spec.rb38
-rw-r--r--spec/features/snippets/create_snippet_spec.rb8
-rw-r--r--spec/features/snippets/public_snippets_spec.rb3
-rw-r--r--spec/features/snippets/show_spec.rb126
-rw-r--r--spec/features/task_lists_spec.rb3
-rw-r--r--spec/features/todos/todos_filtering_spec.rb6
-rw-r--r--spec/features/todos/todos_spec.rb79
-rw-r--r--spec/features/u2f_spec.rb2
-rw-r--r--spec/features/users/projects_spec.rb2
-rw-r--r--spec/features/users/snippets_spec.rb2
-rw-r--r--spec/features/users_spec.rb1
-rw-r--r--spec/features/variables_spec.rb2
-rw-r--r--spec/finders/issues_finder_spec.rb24
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/public.json2
-rw-r--r--spec/fixtures/markdown.md.erb2
-rw-r--r--spec/helpers/application_helper_spec.rb27
-rw-r--r--spec/helpers/blob_helper_spec.rb121
-rw-r--r--spec/helpers/ci_status_helper_spec.rb40
-rw-r--r--spec/helpers/events_helper_spec.rb2
-rw-r--r--spec/helpers/icons_helper_spec.rb15
-rw-r--r--spec/helpers/markup_helper_spec.rb (renamed from spec/helpers/gitlab_markdown_helper_spec.rb)35
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb81
-rw-r--r--spec/helpers/projects_helper_spec.rb14
-rw-r--r--spec/helpers/submodule_helper_spec.rb12
-rw-r--r--spec/javascripts/blob/blob_fork_suggestion_spec.js38
-rw-r--r--spec/javascripts/blob/pdf/index_spec.js6
-rw-r--r--spec/javascripts/blob/pdf/test.pdfbin11956 -> 0 bytes
-rw-r--r--spec/javascripts/blob/sketch/index_spec.js2
-rw-r--r--spec/javascripts/blob/viewer/index_spec.js161
-rw-r--r--spec/javascripts/boards/list_spec.js40
-rw-r--r--spec/javascripts/build_spec.js131
-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.js4
-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.js443
-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/filtered_search_visual_tokens_spec.js16
-rw-r--r--spec/javascripts/filtered_search/services/recent_searches_service_spec.js2
-rw-r--r--spec/javascripts/fixtures/blob.rb29
-rw-r--r--spec/javascripts/fixtures/line_highlighter.html.haml2
-rw-r--r--spec/javascripts/fixtures/pdf.rb18
-rw-r--r--spec/javascripts/fixtures/raw.rb24
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js7
-rw-r--r--spec/javascripts/landing_spec.js160
-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/mini_pipeline_graph_dropdown_spec.js106
-rw-r--r--spec/javascripts/notebook/cells/code_spec.js55
-rw-r--r--spec/javascripts/notebook/cells/markdown_spec.js41
-rw-r--r--spec/javascripts/notebook/cells/output/index_spec.js126
-rw-r--r--spec/javascripts/notebook/cells/prompt_spec.js56
-rw-r--r--spec/javascripts/notebook/index_spec.js98
-rw-r--r--spec/javascripts/notebook/lib/highlight_spec.js15
-rw-r--r--spec/javascripts/notes_spec.js152
-rw-r--r--spec/javascripts/pdf/index_spec.js61
-rw-r--r--spec/javascripts/pdf/page_spec.js57
-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.js81
-rw-r--r--spec/javascripts/pipelines/time_ago_spec.js64
-rw-r--r--spec/javascripts/shortcuts_spec.js45
-rw-r--r--spec/javascripts/user_callout_spec.js1
-rw-r--r--spec/lib/banzai/filter/emoji_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/issuable_state_filter_spec.rb177
-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/renderer_spec.rb69
-rw-r--r--spec/lib/container_registry/path_spec.rb32
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb21
-rw-r--r--spec/lib/gitlab/auth_spec.rb2
-rw-r--r--spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb142
-rw-r--r--spec/lib/gitlab/checks/force_push_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb21
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb17
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb10
-rw-r--r--spec/lib/gitlab/git/encoding_helper_spec.rb4
-rw-r--r--spec/lib/gitlab/git/index_spec.rb20
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb43
-rw-r--r--spec/lib/gitlab/git_access_spec.rb2
-rw-r--r--spec/lib/gitlab/git_ref_validator_spec.rb (renamed from spec/lib/git_ref_validator_spec.rb)0
-rw-r--r--spec/lib/gitlab/health_checks/db_check_spec.rb (renamed from spec/lib/gitlab/healthchecks/db_check_spec.rb)0
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb (renamed from spec/lib/gitlab/healthchecks/fs_shards_check_spec.rb)0
-rw-r--r--spec/lib/gitlab/health_checks/redis_check_spec.rb (renamed from spec/lib/gitlab/healthchecks/redis_check_spec.rb)0
-rw-r--r--spec/lib/gitlab/health_checks/simple_check_shared.rb (renamed from spec/lib/gitlab/healthchecks/simple_check_shared.rb)0
-rw-r--r--spec/lib/gitlab/import_export/project.json41
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb (renamed from spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb)0
-rw-r--r--spec/lib/gitlab/issuable_sorter_spec.rb62
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb25
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb29
-rw-r--r--spec/lib/gitlab/other_markup_spec.rb (renamed from spec/lib/gitlab/other_markup.rb)6
-rw-r--r--spec/lib/gitlab/regex_spec.rb6
-rw-r--r--spec/lib/gitlab/request_profiler_spec.rb27
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb25
-rw-r--r--spec/lib/gitlab/shell_spec.rb (renamed from spec/lib/gitlab/backend/shell_spec.rb)0
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb70
-rw-r--r--spec/lib/gitlab/user_access_spec.rb4
-rw-r--r--spec/lib/gitlab/user_activities_spec.rb127
-rw-r--r--spec/lib/light_url_builder_spec.rb119
-rw-r--r--spec/mailers/emails/merge_requests_spec.rb2
-rw-r--r--spec/mailers/emails/profile_spec.rb146
-rw-r--r--spec/migrations/active_record/schema_spec.rb (renamed from spec/migrations/schema_spec.rb)0
-rw-r--r--spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb49
-rw-r--r--spec/models/application_setting_spec.rb1
-rw-r--r--spec/models/blob_spec.rb258
-rw-r--r--spec/models/blob_viewer/base_spec.rb186
-rw-r--r--spec/models/ci/pipeline_spec.rb91
-rw-r--r--spec/models/concerns/awardable_spec.rb4
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb274
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb6
-rw-r--r--spec/models/concerns/issuable_spec.rb35
-rw-r--r--spec/models/concerns/noteable_spec.rb2
-rw-r--r--spec/models/concerns/relative_positioning_spec.rb2
-rw-r--r--spec/models/concerns/routable_spec.rb18
-rw-r--r--spec/models/concerns/spammable_spec.rb4
-rw-r--r--spec/models/concerns/strip_attribute_spec.rb2
-rw-r--r--spec/models/issue_spec.rb5
-rw-r--r--spec/models/member_spec.rb27
-rw-r--r--spec/models/members/group_member_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb42
-rw-r--r--spec/models/network/graph_spec.rb21
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb20
-rw-r--r--spec/models/project_services/issue_tracker_service_spec.rb2
-rw-r--r--spec/models/project_services/pipelines_email_service_spec.rb (renamed from spec/models/project_services/pipeline_email_service_spec.rb)0
-rw-r--r--spec/models/project_spec.rb15
-rw-r--r--spec/models/repository_spec.rb38
-rw-r--r--spec/models/sent_notification_spec.rb8
-rw-r--r--spec/models/service_spec.rb55
-rw-r--r--spec/models/snippet_blob_spec.rb47
-rw-r--r--spec/models/snippet_spec.rb13
-rw-r--r--spec/models/todo_spec.rb46
-rw-r--r--spec/models/user_spec.rb24
-rw-r--r--spec/policies/issue_policy_spec.rb246
-rw-r--r--spec/policies/issues_policy_spec.rb193
-rw-r--r--spec/requests/api/access_requests_spec.rb4
-rw-r--r--spec/requests/api/award_emoji_spec.rb3
-rw-r--r--spec/requests/api/boards_spec.rb4
-rw-r--r--spec/requests/api/branches_spec.rb4
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb4
-rw-r--r--spec/requests/api/commit_statuses_spec.rb4
-rw-r--r--spec/requests/api/commits_spec.rb6
-rw-r--r--spec/requests/api/deploy_keys_spec.rb4
-rw-r--r--spec/requests/api/deployments_spec.rb4
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb4
-rw-r--r--spec/requests/api/environments_spec.rb4
-rw-r--r--spec/requests/api/files_spec.rb9
-rw-r--r--spec/requests/api/groups_spec.rb3
-rw-r--r--spec/requests/api/helpers/internal_helpers_spec.rb (renamed from spec/requests/api/api_internal_helpers_spec.rb)0
-rw-r--r--spec/requests/api/helpers_spec.rb30
-rw-r--r--spec/requests/api/internal_spec.rb18
-rw-r--r--spec/requests/api/issues_spec.rb87
-rw-r--r--spec/requests/api/jobs_spec.rb4
-rw-r--r--spec/requests/api/keys_spec.rb10
-rw-r--r--spec/requests/api/labels_spec.rb4
-rw-r--r--spec/requests/api/lint_spec.rb4
-rw-r--r--spec/requests/api/members_spec.rb4
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb4
-rw-r--r--spec/requests/api/merge_requests_spec.rb103
-rw-r--r--spec/requests/api/milestones_spec.rb3
-rw-r--r--spec/requests/api/namespaces_spec.rb3
-rw-r--r--spec/requests/api/notes_spec.rb3
-rw-r--r--spec/requests/api/notification_settings_spec.rb4
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb4
-rw-r--r--spec/requests/api/pipelines_spec.rb4
-rw-r--r--spec/requests/api/project_hooks_spec.rb3
-rw-r--r--spec/requests/api/project_snippets_spec.rb4
-rw-r--r--spec/requests/api/projects_spec.rb37
-rw-r--r--spec/requests/api/repositories_spec.rb3
-rw-r--r--spec/requests/api/runner_spec.rb1
-rw-r--r--spec/requests/api/runners_spec.rb4
-rw-r--r--spec/requests/api/services_spec.rb4
-rw-r--r--spec/requests/api/session_spec.rb4
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/requests/api/sidekiq_metrics_spec.rb4
-rw-r--r--spec/requests/api/snippets_spec.rb3
-rw-r--r--spec/requests/api/system_hooks_spec.rb4
-rw-r--r--spec/requests/api/tags_spec.rb3
-rw-r--r--spec/requests/api/templates_spec.rb4
-rw-r--r--spec/requests/api/todos_spec.rb4
-rw-r--r--spec/requests/api/triggers_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb154
-rw-r--r--spec/requests/api/v3/award_emoji_spec.rb4
-rw-r--r--spec/requests/api/v3/boards_spec.rb4
-rw-r--r--spec/requests/api/v3/branches_spec.rb4
-rw-r--r--spec/requests/api/v3/broadcast_messages_spec.rb4
-rw-r--r--spec/requests/api/v3/builds_spec.rb4
-rw-r--r--spec/requests/api/v3/commits_spec.rb6
-rw-r--r--spec/requests/api/v3/deploy_keys_spec.rb4
-rw-r--r--spec/requests/api/v3/deployments_spec.rb14
-rw-r--r--spec/requests/api/v3/environments_spec.rb4
-rw-r--r--spec/requests/api/v3/files_spec.rb10
-rw-r--r--spec/requests/api/v3/groups_spec.rb3
-rw-r--r--spec/requests/api/v3/issues_spec.rb3
-rw-r--r--spec/requests/api/v3/labels_spec.rb4
-rw-r--r--spec/requests/api/v3/members_spec.rb4
-rw-r--r--spec/requests/api/v3/merge_request_diffs_spec.rb4
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb16
-rw-r--r--spec/requests/api/v3/milestones_spec.rb3
-rw-r--r--spec/requests/api/v3/notes_spec.rb4
-rw-r--r--spec/requests/api/v3/pipelines_spec.rb4
-rw-r--r--spec/requests/api/v3/project_hooks_spec.rb3
-rw-r--r--spec/requests/api/v3/project_snippets_spec.rb4
-rw-r--r--spec/requests/api/v3/projects_spec.rb3
-rw-r--r--spec/requests/api/v3/repositories_spec.rb3
-rw-r--r--spec/requests/api/v3/runners_spec.rb4
-rw-r--r--spec/requests/api/v3/services_spec.rb4
-rw-r--r--spec/requests/api/v3/settings_spec.rb4
-rw-r--r--spec/requests/api/v3/snippets_spec.rb3
-rw-r--r--spec/requests/api/v3/system_hooks_spec.rb4
-rw-r--r--spec/requests/api/v3/tags_spec.rb3
-rw-r--r--spec/requests/api/v3/templates_spec.rb4
-rw-r--r--spec/requests/api/v3/todos_spec.rb4
-rw-r--r--spec/requests/api/v3/triggers_spec.rb2
-rw-r--r--spec/requests/api/v3/users_spec.rb10
-rw-r--r--spec/requests/api/variables_spec.rb4
-rw-r--r--spec/requests/api/version_spec.rb4
-rw-r--r--spec/requests/ci/api/builds_spec.rb2
-rw-r--r--spec/requests/ci/api/runners_spec.rb1
-rw-r--r--spec/requests/ci/api/triggers_spec.rb2
-rw-r--r--spec/requests/git_http_spec.rb13
-rw-r--r--spec/requests/openid_connect_spec.rb2
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb4
-rw-r--r--spec/requests/request_profiler_spec.rb44
-rw-r--r--spec/routing/admin_routing_spec.rb14
-rw-r--r--spec/routing/environments_spec.rb2
-rw-r--r--spec/routing/notifications_routing_spec.rb14
-rw-r--r--spec/routing/project_routing_spec.rb6
-rw-r--r--spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb44
-rw-r--r--spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb (renamed from spec/rubocop/cop/migration/add_column_with_default_spec.rb)4
-rw-r--r--spec/serializers/analytics_issue_entity_spec.rb (renamed from spec/serializers/analytics_generic_entity_spec.rb)0
-rw-r--r--spec/serializers/build_serializer_spec.rb2
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb2
-rw-r--r--spec/serializers/status_entity_spec.rb6
-rw-r--r--spec/services/ci/expire_pipeline_cache_service_spec.rb27
-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/issues/resolve_discussions_spec.rb24
-rw-r--r--spec/services/members/authorized_destroy_service_spec.rb45
-rw-r--r--spec/services/merge_requests/build_service_spec.rb10
-rw-r--r--spec/services/merge_requests/resolved_discussion_notification_service_spec.rb (renamed from spec/services/merge_requests/resolved_discussion_notification_service.rb)0
-rw-r--r--spec/services/projects/create_service_spec.rb16
-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/slash_commands/interpret_service_spec.rb86
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/services/users/activity_service_spec.rb48
-rw-r--r--spec/services/users/migrate_to_ghost_user_service_spec.rb18
-rw-r--r--spec/spec_helper.rb5
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb8
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb1
-rw-r--r--spec/support/helpers/fake_blob_helpers.rb50
-rw-r--r--spec/support/login_helpers.rb4
-rw-r--r--spec/support/markdown_feature.rb4
-rw-r--r--spec/support/matchers/access_matchers.rb4
-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.rb11
-rw-r--r--spec/support/user_activities_helpers.rb7
-rw-r--r--spec/support/wait_for_ajax.rb5
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb8
-rw-r--r--spec/unicorn/unicorn_spec.rb98
-rw-r--r--spec/views/layouts/nav/_project.html.haml_spec.rb37
-rw-r--r--spec/views/projects/_last_commit.html.haml_spec.rb22
-rw-r--r--spec/views/projects/blob/_viewer.html.haml_spec.rb97
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb34
-rw-r--r--spec/workers/expire_build_instance_artifacts_worker_spec.rb6
-rw-r--r--spec/workers/expire_pipeline_cache_worker_spec.rb44
-rw-r--r--spec/workers/gitlab_usage_ping_worker_spec.rb23
-rw-r--r--spec/workers/pipeline_process_worker_spec.rb (renamed from spec/workers/pipeline_proccess_worker_spec.rb)0
-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/Dockerfile/CONTRIBUTING.md5
-rw-r--r--vendor/Dockerfile/HTTPd.Dockerfile (renamed from vendor/dockerfile/HTTPdDockerfile)0
-rw-r--r--vendor/Dockerfile/LICENSE21
-rw-r--r--vendor/Dockerfile/PHP.Dockerfile14
-rw-r--r--vendor/Dockerfile/Python2.Dockerfile11
-rw-r--r--vendor/assets/javascripts/notebooklab.js5733
-rw-r--r--vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml4
-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--yarn.lock66
1421 files changed, 20703 insertions, 15130 deletions
diff --git a/.eslintrc b/.eslintrc
index 57a08a06527..aba8112c5a9 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -14,7 +14,8 @@
"plugins": [
"filenames",
"import",
- "html"
+ "html",
+ "promise"
],
"settings": {
"html/html-extensions": [".html", ".html.raw", ".vue"],
@@ -26,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/.gitignore b/.gitignore
index 51b4d06b01b..bb818213de1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,11 +45,12 @@ eslint-report.html
/public/uploads.*
/public/uploads/
/shared/artifacts/
+/spec/javascripts/fixtures/blob/pdf/
/rails_best_practices_output.html
/tags
/tmp/*
/vendor/bundle/*
-/builds/*
+/builds*
/shared/*
/.gitlab_workhorse_secret
/webpack-report/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 712c02046cc..947a8679093 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-golang-1.8-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-postgresql-9.6"
cache:
key: "ruby-233"
@@ -10,21 +10,17 @@ variables:
RAILS_ENV: "test"
NODE_ENV: "test"
SIMPLECOV: "true"
- SETUP_DB: "true"
- USE_BUNDLE_INSTALL: "true"
GIT_DEPTH: "20"
+ GIT_SUBMODULE_STRATEGY: "none"
PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
before_script:
- - source ./scripts/prepare_build.sh
- - cp config/gitlab.yml.example config/gitlab.yml
- bundle --version
- - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) --clean $FLAGS'
- - retry gem install knapsack fog-aws mime-types
- - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql'
+ - source scripts/utils.sh
+ - source scripts/prepare_build.sh
stages:
- prepare
@@ -52,20 +48,41 @@ stages:
paths:
- knapsack/
-.use-db: &use-db
+.use-pg: &use-pg
+ services:
+ - postgres:latest
+ - redis:alpine
+
+.use-mysql: &use-mysql
services:
- mysql:latest
- redis:alpine
+.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
+ only:
+ - /mysql/
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab/gitlabhq
+ - tags@gitlab-org/gitlab-ce
+ - tags@gitlab/gitlabhq
+ - //@gitlab-org/gitlab-ee
+ - //@gitlab/gitlab-ee
+
+# Skip all jobs except the ones that begin with 'docs/'.
+# Used for commits including ONLY documentation changes.
+# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
+.except-docs: &except-docs
+ except:
+ - /^docs\/.*/
+
.rspec-knapsack: &rspec-knapsack
stage: test
<<: *dedicated-runner
- <<: *use-db
script:
- JOB_NAME=( $CI_JOB_NAME )
- - export CI_NODE_INDEX=${JOB_NAME[1]}
- - 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 CI_NODE_INDEX=${JOB_NAME[-2]}
+ - export CI_NODE_TOTAL=${JOB_NAME[-1]}
+ - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_${JOB_NAME[1]}_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}
@@ -78,15 +95,25 @@ stages:
- knapsack/
- tmp/capybara/
+.rspec-knapsack-pg: &rspec-knapsack-pg
+ <<: *rspec-knapsack
+ <<: *use-pg
+ <<: *except-docs
+
+.rspec-knapsack-mysql: &rspec-knapsack-mysql
+ <<: *rspec-knapsack
+ <<: *use-mysql
+ <<: *only-master-and-ee-or-mysql
+ <<: *except-docs
+
.spinach-knapsack: &spinach-knapsack
stage: test
<<: *dedicated-runner
- <<: *use-db
script:
- JOB_NAME=( $CI_JOB_NAME )
- - export CI_NODE_INDEX=${JOB_NAME[1]}
- - 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 CI_NODE_INDEX=${JOB_NAME[-2]}
+ - export CI_NODE_TOTAL=${JOB_NAME[-1]}
+ - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_${JOB_NAME[1]}_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}
@@ -99,10 +126,22 @@ stages:
- knapsack/
- tmp/capybara/
+.spinach-knapsack-pg: &spinach-knapsack-pg
+ <<: *spinach-knapsack
+ <<: *use-pg
+ <<: *except-docs
+
+.spinach-knapsack-mysql: &spinach-knapsack-mysql
+ <<: *spinach-knapsack
+ <<: *use-mysql
+ <<: *only-master-and-ee-or-mysql
+ <<: *except-docs
+
# Prepare and merge knapsack tests
knapsack:
<<: *knapsack-state
<<: *dedicated-runner
+ <<: *except-docs
stage: prepare
script:
- mkdir -p knapsack/${CI_PROJECT_NAME}/
@@ -116,8 +155,8 @@ update-knapsack:
<<: *dedicated-runner
stage: post-test
script:
- - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec_node_*.json
- - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach_node_*.json
+ - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec_pg_node_*.json
+ - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach_pg_node_*.json
- '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
only:
@@ -127,8 +166,9 @@ update-knapsack:
- master@gitlab/gitlab-ee
setup-test-env:
- <<: *use-db
+ <<: *use-pg
<<: *dedicated-runner
+ <<: *except-docs
stage: prepare
script:
- node --version
@@ -142,37 +182,69 @@ setup-test-env:
- public/assets
- tmp/tests
-rspec 0 20: *rspec-knapsack
-rspec 1 20: *rspec-knapsack
-rspec 2 20: *rspec-knapsack
-rspec 3 20: *rspec-knapsack
-rspec 4 20: *rspec-knapsack
-rspec 5 20: *rspec-knapsack
-rspec 6 20: *rspec-knapsack
-rspec 7 20: *rspec-knapsack
-rspec 8 20: *rspec-knapsack
-rspec 9 20: *rspec-knapsack
-rspec 10 20: *rspec-knapsack
-rspec 11 20: *rspec-knapsack
-rspec 12 20: *rspec-knapsack
-rspec 13 20: *rspec-knapsack
-rspec 14 20: *rspec-knapsack
-rspec 15 20: *rspec-knapsack
-rspec 16 20: *rspec-knapsack
-rspec 17 20: *rspec-knapsack
-rspec 18 20: *rspec-knapsack
-rspec 19 20: *rspec-knapsack
-
-spinach 0 10: *spinach-knapsack
-spinach 1 10: *spinach-knapsack
-spinach 2 10: *spinach-knapsack
-spinach 3 10: *spinach-knapsack
-spinach 4 10: *spinach-knapsack
-spinach 5 10: *spinach-knapsack
-spinach 6 10: *spinach-knapsack
-spinach 7 10: *spinach-knapsack
-spinach 8 10: *spinach-knapsack
-spinach 9 10: *spinach-knapsack
+rspec pg 0 20: *rspec-knapsack-pg
+rspec pg 1 20: *rspec-knapsack-pg
+rspec pg 2 20: *rspec-knapsack-pg
+rspec pg 3 20: *rspec-knapsack-pg
+rspec pg 4 20: *rspec-knapsack-pg
+rspec pg 5 20: *rspec-knapsack-pg
+rspec pg 6 20: *rspec-knapsack-pg
+rspec pg 7 20: *rspec-knapsack-pg
+rspec pg 8 20: *rspec-knapsack-pg
+rspec pg 9 20: *rspec-knapsack-pg
+rspec pg 10 20: *rspec-knapsack-pg
+rspec pg 11 20: *rspec-knapsack-pg
+rspec pg 12 20: *rspec-knapsack-pg
+rspec pg 13 20: *rspec-knapsack-pg
+rspec pg 14 20: *rspec-knapsack-pg
+rspec pg 15 20: *rspec-knapsack-pg
+rspec pg 16 20: *rspec-knapsack-pg
+rspec pg 17 20: *rspec-knapsack-pg
+rspec pg 18 20: *rspec-knapsack-pg
+rspec pg 19 20: *rspec-knapsack-pg
+
+rspec mysql 0 20: *rspec-knapsack-mysql
+rspec mysql 1 20: *rspec-knapsack-mysql
+rspec mysql 2 20: *rspec-knapsack-mysql
+rspec mysql 3 20: *rspec-knapsack-mysql
+rspec mysql 4 20: *rspec-knapsack-mysql
+rspec mysql 5 20: *rspec-knapsack-mysql
+rspec mysql 6 20: *rspec-knapsack-mysql
+rspec mysql 7 20: *rspec-knapsack-mysql
+rspec mysql 8 20: *rspec-knapsack-mysql
+rspec mysql 9 20: *rspec-knapsack-mysql
+rspec mysql 10 20: *rspec-knapsack-mysql
+rspec mysql 11 20: *rspec-knapsack-mysql
+rspec mysql 12 20: *rspec-knapsack-mysql
+rspec mysql 13 20: *rspec-knapsack-mysql
+rspec mysql 14 20: *rspec-knapsack-mysql
+rspec mysql 15 20: *rspec-knapsack-mysql
+rspec mysql 16 20: *rspec-knapsack-mysql
+rspec mysql 17 20: *rspec-knapsack-mysql
+rspec mysql 18 20: *rspec-knapsack-mysql
+rspec mysql 19 20: *rspec-knapsack-mysql
+
+spinach pg 0 10: *spinach-knapsack-pg
+spinach pg 1 10: *spinach-knapsack-pg
+spinach pg 2 10: *spinach-knapsack-pg
+spinach pg 3 10: *spinach-knapsack-pg
+spinach pg 4 10: *spinach-knapsack-pg
+spinach pg 5 10: *spinach-knapsack-pg
+spinach pg 6 10: *spinach-knapsack-pg
+spinach pg 7 10: *spinach-knapsack-pg
+spinach pg 8 10: *spinach-knapsack-pg
+spinach pg 9 10: *spinach-knapsack-pg
+
+spinach mysql 0 10: *spinach-knapsack-mysql
+spinach mysql 1 10: *spinach-knapsack-mysql
+spinach mysql 2 10: *spinach-knapsack-mysql
+spinach mysql 3 10: *spinach-knapsack-mysql
+spinach mysql 4 10: *spinach-knapsack-mysql
+spinach mysql 5 10: *spinach-knapsack-mysql
+spinach mysql 6 10: *spinach-knapsack-mysql
+spinach mysql 7 10: *spinach-knapsack-mysql
+spinach mysql 8 10: *spinach-knapsack-mysql
+spinach mysql 9 10: *spinach-knapsack-mysql
# Other generic tests
.ruby-static-analysis: &ruby-static-analysis
@@ -181,29 +253,32 @@ spinach 9 10: *spinach-knapsack
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
-.exec: &exec
+.rake-exec: &rake-exec
<<: *ruby-static-analysis
<<: *dedicated-runner
+ <<: *except-docs
stage: test
script:
- - bundle exec $CI_JOB_NAME
+ - bundle exec rake $CI_JOB_NAME
-rubocop:
+static-analysis:
<<: *ruby-static-analysis
<<: *dedicated-runner
+ <<: *except-docs
stage: test
script:
- - bundle exec "rubocop --require rubocop-rspec"
-
-rake haml_lint: *exec
-rake scss_lint: *exec
-rake config_lint: *exec
-rake brakeman: *exec
-rake flay: *exec
-license_finder: *exec
-rake downtime_check: *exec
-rake ee_compat_check:
- <<: *exec
+ - scripts/static-analysis
+
+downtime_check:
+ <<: *rake-exec
+ except:
+ - master
+ - tags
+ - /^[\d-]+-stable(-ee)?$/
+ - /^docs\/*/
+
+ee_compat_check:
+ <<: *rake-exec
only:
- branches@gitlab-org/gitlab-ce
except:
@@ -222,24 +297,41 @@ rake ee_compat_check:
paths:
- ee_compat_check/patches/*.patch
-rake db:migrate:reset:
+.db-migrate-reset: &db-migrate-reset
stage: test
- <<: *use-db
<<: *dedicated-runner
script:
- bundle exec rake db:migrate:reset
-rake db:rollback:
+db:migrate:reset pg:
+ <<: *db-migrate-reset
+ <<: *use-pg
+ <<: *except-docs
+
+db:migrate:reset mysql:
+ <<: *db-migrate-reset
+ <<: *use-mysql
+ <<: *except-docs
+
+.db-rollback: &db-rollback
stage: test
- <<: *use-db
<<: *dedicated-runner
script:
- bundle exec rake db:rollback STEP=120
- bundle exec rake db:migrate
-rake db:seed_fu:
+db:rollback pg:
+ <<: *db-rollback
+ <<: *use-pg
+ <<: *except-docs
+
+db:rollback mysql:
+ <<: *db-rollback
+ <<: *use-mysql
+ <<: *except-docs
+
+.db-seed_fu: &db-seed_fu
stage: test
- <<: *use-db
<<: *dedicated-runner
variables:
SIZE: "1"
@@ -255,9 +347,20 @@ rake db:seed_fu:
paths:
- log/development.log
-rake gitlab:assets:compile:
+db:seed_fu pg:
+ <<: *db-seed_fu
+ <<: *use-pg
+ <<: *except-docs
+
+db:seed_fu mysql:
+ <<: *db-seed_fu
+ <<: *use-mysql
+ <<: *except-docs
+
+gitlab:assets:compile:
stage: test
<<: *dedicated-runner
+ <<: *except-docs
dependencies: []
variables:
NODE_ENV: "production"
@@ -274,14 +377,14 @@ rake gitlab:assets:compile:
paths:
- webpack-report/
-rake karma:
+karma:
cache:
paths:
- vendor/ruby
- - node_modules
stage: test
- <<: *use-db
+ <<: *use-pg
<<: *dedicated-runner
+ <<: *except-docs
variables:
BABEL_ENV: "coverage"
script:
@@ -293,16 +396,6 @@ rake karma:
paths:
- coverage-javascript/
-docs:check:apilint:
- image: "phusion/baseimage"
- stage: test
- <<: *dedicated-runner
- cache: {}
- dependencies: []
- before_script: []
- script:
- - scripts/lint-doc.sh
-
docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
@@ -318,13 +411,6 @@ docs:check:links:
# Check the internal links
- bundle exec nanoc check internal_links
-bundler:check:
- stage: test
- <<: *dedicated-runner
- <<: *ruby-static-analysis
- script:
- - bundle check
-
bundler:audit:
stage: test
<<: *ruby-static-analysis
@@ -337,9 +423,8 @@ bundler:audit:
script:
- "bundle exec bundle-audit check --update --ignore CVE-2016-4658"
-migration paths:
+.migration-paths: &migration-paths
stage: test
- <<: *use-db
<<: *dedicated-runner
variables:
SETUP_DB: "false"
@@ -351,17 +436,26 @@ migration paths:
script:
- git fetch origin v8.14.10
- git checkout -f FETCH_HEAD
- - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
+ - bundle install $BUNDLE_INSTALL_FLAGS
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_COMMIT_SHA
- - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- - source scripts/prepare_build.sh
+ - bundle install $BUNDLE_INSTALL_FLAGS
+ - . scripts/prepare_build.sh
- bundle exec rake db:migrate
+migration path pg:
+ <<: *migration-paths
+ <<: *use-pg
+
+migration path mysql:
+ <<: *migration-paths
+ <<: *use-mysql
+
coverage:
stage: post-test
services: []
<<: *dedicated-runner
+ <<: *except-docs
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
@@ -375,21 +469,9 @@ coverage:
- coverage/index.html
- coverage/assets/
-lint:javascript:
- <<: *dedicated-runner
- cache:
- paths:
- - node_modules/
- stage: test
- before_script: []
- script:
- - yarn run eslint
-
lint:javascript:report:
<<: *dedicated-runner
- cache:
- paths:
- - node_modules/
+ <<: *except-docs
stage: post-test
before_script: []
script:
@@ -410,7 +492,7 @@ trigger_docs:
before_script:
- apk update && apk add curl
variables:
- GIT_STRATEGY: none
+ GIT_STRATEGY: "none"
cache: {}
artifacts: {}
script:
@@ -426,8 +508,8 @@ pages:
<<: *dedicated-runner
dependencies:
- coverage
- - rake karma
- - rake gitlab:assets:compile
+ - karma
+ - gitlab:assets:compile
- lint:javascript:report
script:
- mv public/ .public/
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index 6bb21e6a3af..66e1e0e20b3 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -1,3 +1,17 @@
+Please read this!
+
+Before opening a new issue, make sure to search for keywords in the issues
+filtered by the "regression" or "bug" label:
+
+- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression
+- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug
+
+and verify the issue you're about to submit isn't a duplicate.
+
+Please remove this notice if you're confident your issue isn't a duplicate.
+
+------
+
### Summary
(Summarize the bug encountered concisely)
@@ -26,6 +40,7 @@ logs, and code as it's very hard to read otherwise.)
#### Results of GitLab environment info
<details>
+<pre>
(For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:env:info`)
@@ -33,11 +48,13 @@ logs, and code as it's very hard to read otherwise.)
(For installations from source run and paste the output of:
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
+</pre>
</details>
#### Results of GitLab application Check
<details>
+<pre>
(For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:check SANITIZE=true`)
@@ -47,8 +64,11 @@ logs, and code as it's very hard to read otherwise.)
(we will only investigate if the tests are passing)
+</pre>
</details>
### Possible fixes
(If you can, link to the line of code that might be responsible for the problem)
+
+/label ~bug
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
index 2636010e2fb..d96c9ad59e0 100644
--- a/.gitlab/issue_templates/Feature Proposal.md
+++ b/.gitlab/issue_templates/Feature Proposal.md
@@ -1,3 +1,16 @@
+Please read this!
+
+Before opening a new issue, make sure to search for keywords in the issues
+filtered by the "feature proposal" label:
+
+- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal
+
+and verify the issue you're about to submit isn't a duplicate.
+
+Please remove this notice if you're confident your issue isn't a duplicate.
+
+------
+
### Description
(Include problem, use cases, benefits, and/or goals)
@@ -15,3 +28,5 @@
3. How does someone use this
During implementation, this can then be copied and used as a starter for the documentation.)
+
+/label ~"feature proposal"
diff --git a/.rubocop.yml b/.rubocop.yml
index e5549b64503..8c43f6909cf 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -543,7 +543,7 @@ Style/Proc:
# branches, and conditions.
Metrics/AbcSize:
Enabled: true
- Max: 60
+ Max: 57.08
# This cop checks if the length of a block exceeds some maximum value.
Metrics/BlockLength:
@@ -562,7 +562,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method.
Metrics/CyclomaticComplexity:
Enabled: true
- Max: 17
+ Max: 16
# Limit lines to 80 characters.
Metrics/LineLength:
@@ -983,10 +983,12 @@ RSpec/ExpectActual:
# Checks the file and folder naming of the spec file.
RSpec/FilePath:
- Enabled: false
- CustomTransform:
- RuboCop: rubocop
- RSpec: rspec
+ Enabled: true
+ IgnoreMethods: true
+ Exclude:
+ - 'qa/**/*'
+ - 'spec/javascripts/fixtures/*'
+ - 'spec/requests/api/v3/*'
# Checks if there are focused specs.
RSpec/Focus:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4047a5b6f32..2686d778b09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,292 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 9.1.2 (2017-05-01)
+
+- Add index on ci_runners.contacted_at. !10876 (blackst0ne)
+- Fix pipeline events description for Slack and Mattermost integration. !10908
+- Fixed milestone sidebar showing incorrect number of MRs when collapsed. !10933
+- Fix ordering of commits in the network graph. !10936
+- Ensure the chat notifications service properly saves the "Notify only default branch" setting. !10959
+- Lazily sets UUID in ApplicationSetting for new installations.
+- Skip validation when creating internal (ghost, service desk) users.
+- Use GitLab Pages v0.4.1.
+
+## 9.1.1 (2017-04-26)
+
+- Add a transaction around move_issues_to_ghost_user. !10465
+- Properly expire cache for all MRs of a pipeline. !10770
+- Add sub-nav for Project Integration Services edit page. !10813
+- Fix missing duration for blocked pipelines. !10856
+- Fix lastest commit status text on main project page. !10863
+- Add index on ci_builds.updated_at. !10870 (blackst0ne)
+- Fix 500 error due to trying to show issues from pending deleting projects. !10906
+- Ensures that OAuth/LDAP/SAML users don't need to be confirmed.
+- Ensure replying to an individual note by email creates a note with its own discussion ID.
+- Fix OAuth, LDAP and SAML SSO when regular sign-ups are disabled.
+- Fix usage ping docs link from empty cohorts page.
+- Eliminate N+1 queries in loading namespaces for every issuable in milestones.
+
+## 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.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a918a2aa18d..a3df0a6959e 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.6.0
+0.8.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 1d0ba9ea182..267577d47e4 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.4.0
+0.4.1
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index a1ef0cae183..50e2274e6d3 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-5.0.2
+5.0.3
diff --git a/Gemfile b/Gemfile
index ad8db206da6..f54a1f500fd 100644
--- a/Gemfile
+++ b/Gemfile
@@ -17,6 +17,8 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1'
+gem 'faraday', '~> 0.11.0'
+
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
@@ -83,14 +85,14 @@ gem 'kaminari', '~> 0.17.0'
gem 'hamlit', '~> 2.6.1'
# Files attachments
-gem 'carrierwave', '~> 0.11.0'
+gem 'carrierwave', '~> 1.0'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
# for backups
gem 'fog-aws', '~> 0.9'
-gem 'fog-core', '~> 1.40'
+gem 'fog-core', '~> 1.44'
gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
@@ -142,7 +144,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
-gem 'sidekiq', '~> 4.2.7'
+gem 'sidekiq', '~> 5.0'
gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4'
@@ -186,7 +188,7 @@ gem 'gemnasium-gitlab-service', '~> 0.2'
gem 'slack-notifier', '~> 1.5.1'
# Asana integration
-gem 'asana', '~> 0.4.0'
+gem 'asana', '~> 0.6.0'
# FogBugz integration
gem 'ruby-fogbugz', '~> 0.2.1'
@@ -291,6 +293,7 @@ group :development, :test do
gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5'
+ gem 'rspec-set', '~> 0.1.3'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
@@ -345,7 +348,7 @@ gem 'html2text'
gem 'ruby-prof', '~> 0.16.2'
# OAuth
-gem 'oauth2', '~> 1.2.0'
+gem 'oauth2', '~> 1.3.0'
# Soft deletion
gem 'paranoia', '~> 2.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index bb91db1e805..b822a325861 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -47,7 +47,7 @@ GEM
akismet (2.0.0)
allocations (1.0.5)
arel (6.0.4)
- asana (0.4.0)
+ asana (0.6.0)
faraday (~> 0.9)
faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0)
@@ -105,12 +105,10 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
- carrierwave (0.11.2)
- activemodel (>= 3.2.0)
- activesupport (>= 3.2.0)
- json (>= 1.7)
+ carrierwave (1.0.0)
+ activemodel (>= 4.0.0)
+ activesupport (>= 4.0.0)
mime-types (>= 1.16)
- mimemagic (>= 0.3.0)
cause (0.1)
charlock_holmes (0.7.3)
chronic (0.10.2)
@@ -184,7 +182,7 @@ GEM
erubis (2.7.0)
escape_utils (1.1.1)
eventmachine (1.0.8)
- excon (0.52.0)
+ excon (0.55.0)
execjs (2.6.0)
expression_parser (0.9.0)
extlib (0.9.16)
@@ -193,10 +191,10 @@ GEM
factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0)
railties (>= 3.0.0)
- faraday (0.9.2)
+ faraday (0.11.0)
multipart-post (>= 1.2, < 3)
- faraday_middleware (0.10.0)
- faraday (>= 0.7.4, < 0.10)
+ faraday_middleware (0.11.0.1)
+ faraday (>= 0.7.4, < 1.0)
faraday_middleware-multi_json (0.0.6)
faraday_middleware
multi_json
@@ -210,12 +208,12 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
- fog-aws (0.11.0)
+ fog-aws (0.13.0)
fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
- fog-core (1.42.0)
+ fog-core (1.44.1)
builder
excon (~> 0.49)
formatador (~> 0.2)
@@ -237,9 +235,9 @@ GEM
fog-json (>= 1.0)
fog-xml (>= 0.1)
ipaddress (>= 0.8)
- fog-xml (0.1.2)
+ fog-xml (0.1.3)
fog-core
- nokogiri (~> 1.5, >= 1.5.11)
+ nokogiri (>= 1.5.11, < 2.0.0)
font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1)
foreman (0.78.0)
@@ -330,7 +328,7 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
- grpc (1.1.2)
+ grpc (1.2.5)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
@@ -429,7 +427,7 @@ GEM
multi_json (~> 1.10)
loofah (2.0.3)
nokogiri (>= 1.5.9)
- mail (2.6.4)
+ mail (2.6.5)
mime-types (>= 1.16, < 4)
mail_room (0.9.1)
memoist (0.15.0)
@@ -454,15 +452,15 @@ GEM
mini_portile2 (~> 2.1.0)
numerizer (0.1.1)
oauth (0.5.1)
- oauth2 (1.2.0)
- faraday (>= 0.8, < 0.10)
+ oauth2 (1.3.1)
+ faraday (>= 0.8, < 0.12)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3)
- oj (2.17.4)
+ oj (2.17.5)
omniauth (1.4.2)
hashie (>= 1.2, < 4)
rack (>= 1.0, < 3)
@@ -603,7 +601,7 @@ GEM
json
recursive-open-struct (1.0.0)
redcarpet (3.4.0)
- redis (3.2.2)
+ redis (3.3.3)
redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
@@ -659,6 +657,7 @@ GEM
rspec-support (~> 3.5.0)
rspec-retry (0.4.5)
rspec-core
+ rspec-set (0.1.3)
rspec-support (3.5.0)
rspec_profiling (0.0.5)
activerecord
@@ -716,11 +715,11 @@ GEM
rack
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
- sidekiq (4.2.7)
+ sidekiq (5.0.0)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
- redis (~> 3.2, >= 3.2.1)
+ redis (~> 3.3, >= 3.3.3)
sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24)
@@ -853,7 +852,7 @@ DEPENDENCIES
after_commit_queue (~> 1.3.0)
akismet (~> 2.0)
allocations (~> 1.0)
- asana (~> 0.4.0)
+ asana (~> 0.6.0)
asciidoctor (~> 1.5.2)
asciidoctor-plantuml (= 0.0.7)
attr_encrypted (~> 3.0.0)
@@ -870,7 +869,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0)
capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
- carrierwave (~> 0.11.0)
+ carrierwave (~> 1.0)
charlock_holmes (~> 0.7.3)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
@@ -891,10 +890,11 @@ DEPENDENCIES
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
+ faraday (~> 0.11.0)
ffaker (~> 2.4)
flay (~> 2.8.0)
fog-aws (~> 0.9)
- fog-core (~> 1.40)
+ fog-core (~> 1.44)
fog-google (~> 0.5)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
@@ -943,7 +943,7 @@ DEPENDENCIES
mysql2 (~> 0.3.16)
net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2)
- oauth2 (~> 1.2.0)
+ oauth2 (~> 1.3.0)
octokit (~> 4.6.2)
oj (~> 2.17.4)
omniauth (~> 1.4.2)
@@ -988,6 +988,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
+ rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
rubocop (~> 0.47.1)
rubocop-rspec (~> 1.15.0)
@@ -1004,7 +1005,7 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
- sidekiq (~> 4.2.7)
+ sidekiq (~> 5.0)
sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4)
simplecov (~> 0.14.0)
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..59de828e1ac 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
+[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Test coverage
@@ -73,7 +74,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/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico
new file mode 100644
index 00000000000..4af3582b60d
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico
new file mode 100644
index 00000000000..13639da2e8a
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_created.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico
new file mode 100644
index 00000000000..5f0e711b104
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico
new file mode 100644
index 00000000000..8b1168a1267
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico
new file mode 100644
index 00000000000..ed19b69e1c5
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico
new file mode 100644
index 00000000000..5dfefd4cc5a
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico
new file mode 100644
index 00000000000..a41539c0e3e
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_running.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico
new file mode 100644
index 00000000000..2c1ae552b93
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico
new file mode 100644
index 00000000000..70f0ca61eca
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_success.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico
new file mode 100644
index 00000000000..db289e03eb1
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico
new file mode 100644
index 00000000000..23adcffff50
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_canceled.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico
new file mode 100644
index 00000000000..f9d93b390d8
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_created.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico
new file mode 100644
index 00000000000..28a22ebf724
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_failed.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico
new file mode 100644
index 00000000000..dbbf1abf30c
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_manual.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico
new file mode 100644
index 00000000000..49b9b232dd1
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_not_found.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico
new file mode 100644
index 00000000000..05962f3f148
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_pending.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico
new file mode 100644
index 00000000000..7fa3d4d48d4
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_running.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico
new file mode 100644
index 00000000000..b0c26b62068
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_skipped.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico
new file mode 100644
index 00000000000..b150960b5be
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_success.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico
new file mode 100644
index 00000000000..7e71d71684d
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_warning.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_canceled.ico b/app/assets/images/ci_favicons/icon_status_canceled.ico
deleted file mode 100755
index 5a19458f2a2..00000000000
--- a/app/assets/images/ci_favicons/icon_status_canceled.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_created.ico b/app/assets/images/ci_favicons/icon_status_created.ico
deleted file mode 100755
index 4dca9640cb3..00000000000
--- a/app/assets/images/ci_favicons/icon_status_created.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_failed.ico b/app/assets/images/ci_favicons/icon_status_failed.ico
deleted file mode 100755
index c961ff9a69b..00000000000
--- a/app/assets/images/ci_favicons/icon_status_failed.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_manual.ico b/app/assets/images/ci_favicons/icon_status_manual.ico
deleted file mode 100755
index 5fbbc99ea7c..00000000000
--- a/app/assets/images/ci_favicons/icon_status_manual.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_not_found.ico b/app/assets/images/ci_favicons/icon_status_not_found.ico
deleted file mode 100755
index 21afa9c72e6..00000000000
--- a/app/assets/images/ci_favicons/icon_status_not_found.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_pending.ico b/app/assets/images/ci_favicons/icon_status_pending.ico
deleted file mode 100755
index 8be32dab85a..00000000000
--- a/app/assets/images/ci_favicons/icon_status_pending.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_running.ico b/app/assets/images/ci_favicons/icon_status_running.ico
deleted file mode 100755
index f328ff1a5ed..00000000000
--- a/app/assets/images/ci_favicons/icon_status_running.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_skipped.ico b/app/assets/images/ci_favicons/icon_status_skipped.ico
deleted file mode 100755
index b4394e1b4af..00000000000
--- a/app/assets/images/ci_favicons/icon_status_skipped.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_success.ico b/app/assets/images/ci_favicons/icon_status_success.ico
deleted file mode 100755
index 4f436c95242..00000000000
--- a/app/assets/images/ci_favicons/icon_status_success.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_warning.ico b/app/assets/images/ci_favicons/icon_status_warning.ico
deleted file mode 100755
index 805cc20cdec..00000000000
--- a/app/assets/images/ci_favicons/icon_status_warning.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index f93208944a1..adb45b0606d 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -239,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}`);
});
};
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..47c431fb809 100644
--- a/app/assets/javascripts/blob/blob_fork_suggestion.js
+++ b/app/assets/javascripts/blob/blob_fork_suggestion.js
@@ -1,14 +1,59 @@
-function BlobForkSuggestion(openButton, cancelButton, suggestionSection) {
- if (openButton) {
- openButton.addEventListener('click', () => {
- suggestionSection.classList.remove('hidden');
- });
+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.onOpenButtonClick = this.onOpenButtonClick.bind(this);
+ this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
+ }
+
+ init() {
+ this.bindEvents();
+
+ return this;
+ }
+
+ bindEvents() {
+ $(this.elementMap.openButtons).on('click', this.onOpenButtonClick);
+ $(this.elementMap.cancelButtons).on('click', this.onCancelButtonClick);
+ }
+
+ showSuggestionSection(forkPath, action = 'edit') {
+ $(this.elementMap.suggestionSections).removeClass('hidden');
+ $(this.elementMap.forkButtons).attr('href', forkPath);
+ $(this.elementMap.actionTextPieces).text(action);
+ }
+
+ hideSuggestionSection() {
+ $(this.elementMap.suggestionSections).addClass('hidden');
+ }
+
+ onOpenButtonClick(e) {
+ const forkPath = $(e.currentTarget).attr('data-fork-path');
+ const action = $(e.currentTarget).attr('data-action');
+ this.showSuggestionSection(forkPath, action);
+ }
+
+ onCancelButtonClick() {
+ this.hideSuggestionSection();
}
- if (cancelButton) {
- cancelButton.addEventListener('click', () => {
- suggestionSection.classList.add('hidden');
- });
+ destroy() {
+ $(this.elementMap.openButtons).off('click', this.onOpenButtonClick);
+ $(this.elementMap.cancelButtons).off('click', this.onCancelButtonClick);
}
}
diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js
index 9b8bfbfc8c0..36fe8a7184f 100644
--- a/app/assets/javascripts/blob/notebook/index.js
+++ b/app/assets/javascripts/blob/notebook/index.js
@@ -1,10 +1,9 @@
/* eslint-disable no-new */
import Vue from 'vue';
import VueResource from 'vue-resource';
-import NotebookLab from 'vendor/notebooklab';
+import notebookLab from '../../notebook/index.vue';
Vue.use(VueResource);
-Vue.use(NotebookLab);
export default () => {
const el = document.getElementById('js-notebook-viewer');
@@ -19,6 +18,9 @@ export default () => {
json: {},
};
},
+ components: {
+ notebookLab,
+ },
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js
index a74c2db9a61..0ed915c1ac9 100644
--- a/app/assets/javascripts/blob/pdf/index.js
+++ b/app/assets/javascripts/blob/pdf/index.js
@@ -1,11 +1,6 @@
/* eslint-disable no-new */
import Vue from 'vue';
-import PDFLab from 'vendor/pdflab';
-import workerSrc from 'vendor/pdf.worker';
-
-Vue.use(PDFLab, {
- workerSrc,
-});
+import pdfLab from '../../pdf/index.vue';
export default () => {
const el = document.getElementById('js-pdf-viewer');
@@ -20,6 +15,9 @@ export default () => {
pdf: el.dataset.endpoint,
};
},
+ components: {
+ pdfLab,
+ },
methods: {
onLoad() {
this.loading = false;
@@ -31,7 +29,7 @@ export default () => {
},
},
template: `
- <div class="container-fluid md prepend-top-default append-bottom-default">
+ <div class="js-pdf-viewer container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
new file mode 100644
index 00000000000..07d67d49aa5
--- /dev/null
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -0,0 +1,120 @@
+/* global Flash */
+export default class BlobViewer {
+ constructor() {
+ this.switcher = document.querySelector('.js-blob-viewer-switcher');
+ this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn');
+ this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn');
+ this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]');
+ this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
+ this.$fileHolder = $('.file-holder');
+
+ let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type');
+
+ this.initBindings();
+
+ if (this.switcher && location.hash.indexOf('#L') === 0) {
+ initialViewerName = 'simple';
+ }
+
+ this.switchToViewer(initialViewerName);
+ }
+
+ initBindings() {
+ if (this.switcherBtns.length) {
+ Array.from(this.switcherBtns)
+ .forEach((el) => {
+ el.addEventListener('click', this.switchViewHandler.bind(this));
+ });
+ }
+
+ if (this.copySourceBtn) {
+ this.copySourceBtn.addEventListener('click', () => {
+ if (this.copySourceBtn.classList.contains('disabled')) return;
+
+ this.switchToViewer('simple');
+ });
+ }
+ }
+
+ switchViewHandler(e) {
+ const target = e.currentTarget;
+
+ e.preventDefault();
+
+ this.switchToViewer(target.getAttribute('data-viewer'));
+ }
+
+ toggleCopyButtonState() {
+ if (!this.copySourceBtn) return;
+
+ if (this.simpleViewer.getAttribute('data-loaded')) {
+ this.copySourceBtn.setAttribute('title', 'Copy source to clipboard');
+ this.copySourceBtn.classList.remove('disabled');
+ } else if (this.activeViewer === this.simpleViewer) {
+ this.copySourceBtn.setAttribute('title', 'Wait for the source to load to copy it to the clipboard');
+ this.copySourceBtn.classList.add('disabled');
+ } else {
+ this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard');
+ this.copySourceBtn.classList.add('disabled');
+ }
+
+ $(this.copySourceBtn).tooltip('fixTitle');
+ }
+
+ loadViewer(viewerParam) {
+ const viewer = viewerParam;
+ const url = viewer.getAttribute('data-url');
+
+ if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
+ return;
+ }
+
+ viewer.setAttribute('data-loading', 'true');
+
+ $.ajax({
+ url,
+ dataType: 'JSON',
+ })
+ .fail(() => new Flash('Error loading source view'))
+ .done((data) => {
+ viewer.innerHTML = data.html;
+ $(viewer).syntaxHighlight();
+
+ viewer.setAttribute('data-loaded', 'true');
+
+ this.$fileHolder.trigger('highlight:line');
+
+ this.toggleCopyButtonState();
+ });
+ }
+
+ switchToViewer(name) {
+ const newViewer = document.querySelector(`.blob-viewer[data-type='${name}']`);
+ if (this.activeViewer === newViewer) return;
+
+ const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active');
+ const newButton = document.querySelector(`.js-blob-viewer-switch-btn[data-viewer='${name}']`);
+ const oldViewer = document.querySelector(`.blob-viewer:not([data-type='${name}'])`);
+
+ if (oldButton) {
+ oldButton.classList.remove('active');
+ }
+
+ if (newButton) {
+ newButton.classList.add('active');
+ newButton.blur();
+ }
+
+ if (oldViewer) {
+ oldViewer.classList.add('hidden');
+ }
+
+ newViewer.classList.remove('hidden');
+
+ this.activeViewer = newViewer;
+
+ this.toggleCopyButtonState();
+
+ this.loadViewer(newViewer);
+ }
+}
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index b749ef43cd3..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';
@@ -93,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_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/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index fb0aac3c0e4..fdab317dc23 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -51,11 +51,13 @@ gl.issueBoards.IssuesModal = Vue.extend({
showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
+ const loadingDone = () => {
+ this.loading = false;
+ };
this.loadIssues()
- .then(() => {
- this.loading = false;
- });
+ .then(loadingDone)
+ .catch(loadingDone);
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
@@ -67,11 +69,13 @@ gl.issueBoards.IssuesModal = Vue.extend({
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
+ const loadingDone = () => {
+ this.filterLoading = false;
+ };
this.loadIssues(true)
- .then(() => {
- this.filterLoading = false;
- });
+ .then(loadingDone)
+ .catch(loadingDone);
}
},
deep: true,
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 22f20305624..7e3bb79af1d 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -1,4 +1,5 @@
-/* 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 || {};
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 66384d9c038..ccb00099215 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -36,6 +36,9 @@ gl.issueBoards.BoardsStore = {
.save()
.then(() => {
this.state.lists = _.sortBy(this.state.lists, 'position');
+ })
+ .catch(() => {
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
});
this.removeBlankState();
},
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 0aad95c2fe3..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);
@@ -98,15 +101,22 @@ window.Build = (function () {
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 1d16c64e07e..e704be8b53e 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.vue';
-import ErrorState from '../../vue_pipelines_index/components/error_state.vue';
+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;
},
},
@@ -98,15 +106,6 @@ export default Vue.component('pipelines-table', {
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
- beforeUpdate() {
- if (this.state.pipelines.length &&
- this.$children &&
- !this.isMakingRequest &&
- !this.isLoading) {
- this.store.startTimeAgoLoops.call(this, Vue);
- }
- },
-
beforeDestroyed() {
eventHub.$off('refreshPipelines');
},
@@ -145,8 +144,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 +158,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/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 dcfc40c1013..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: 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];
- },
- note: function () {
- return this.discussion ? this.discussion.getNote(this.noteId) : {};
- },
- 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;
+ }
+ },
+ resolvedByName: function () {
+ return this.note.resolved_by;
},
- methods: {
- updateTooltip: function () {
- this.$nextTick(() => {
- $(this.$refs.button)
- .tooltip('hide')
- .tooltip('fixTitle');
- });
- },
- resolve: function () {
- if (!this.canResolve) return;
+ },
+ 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.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..0bdce52cc89 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';
@@ -43,9 +44,12 @@ import GroupsList from './groups_list';
import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
+import Landing from './landing';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
+import ShortcutsWiki from './shortcuts_wiki';
+import BlobViewer from './blob/viewer/index';
const ShortcutsBlob = require('./shortcuts_blob');
@@ -89,11 +93,14 @@ 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'),
+ })
+ .init();
}
switch (page) {
@@ -142,19 +149,30 @@ const ShortcutsBlob = require('./shortcuts_blob');
new ProjectsList();
break;
case 'dashboard:groups:index':
+ new GroupsList();
+ break;
case 'explore:groups:index':
new GroupsList();
+
+ const landingElement = document.querySelector('.js-explore-groups-landing');
+ if (!landingElement) break;
+ const exploreGroupsLanding = new Landing(
+ landingElement,
+ landingElement.querySelector('.dismiss-button'),
+ 'explore_groups_landing_dismissed',
+ );
+ exploreGroupsLanding.toggle();
break;
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;
@@ -294,6 +312,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:show':
+ new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
initBlob();
break;
@@ -349,6 +368,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'users:show':
new UserCallout();
break;
+ case 'snippets:show':
+ new LineHighlighter();
+ new BlobViewer();
+ break;
}
switch (path.first()) {
case 'sessions':
@@ -365,6 +388,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 +442,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;
@@ -424,6 +450,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
shortcut_handler = new ShortcutsNavigation();
if (path[2] === 'show') {
new ZenMode();
+ new LineHighlighter();
+ new BlobViewer();
}
break;
case 'labels':
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 df0e3f46827..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: "",
@@ -130,13 +133,15 @@ window.DropzoneInput = (function() {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text;
if (shouldPad) formattedText += "\n\n";
- caretStart = $(child)[0].selectionStart;
- caretEnd = $(child)[0].selectionEnd;
+ 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) {
@@ -180,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) {
@@ -215,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.vue
index 0518422e475..f319d6ca0c8 100644
--- a/app/assets/javascripts/environments/components/environment.js
+++ b/app/assets/javascripts/environments/components/environment.vue
@@ -1,14 +1,15 @@
+<script>
+
/* eslint-disable no-new */
/* 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';
import eventHub from '../event_hub';
-export default Vue.component('environment-component', {
+export default {
components: {
'environment-table': EnvironmentTable,
@@ -140,76 +141,90 @@ export default Vue.component('environment-component', {
});
},
},
-
- template: `
- <div :class="cssContainerClass">
- <div class="top-area">
- <ul v-if="!isLoading" class="nav-links">
- <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
- <a :href="projectEnvironmentsPath">
- Available
- <span class="badge js-available-environments-count">
- {{state.availableCounter}}
- </span>
- </a>
- </li>
- <li v-bind:class="{ 'active' : scope === 'stopped' }">
- <a :href="projectStoppedEnvironmentsPath">
- Stopped
- <span class="badge js-stopped-environments-count">
- {{state.stoppedCounter}}
- </span>
- </a>
- </li>
- </ul>
- <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
- <a :href="newEnvironmentPath" class="btn btn-create">
- New environment
+};
+</script>
+<template>
+ <div :class="cssContainerClass">
+ <div class="top-area">
+ <ul
+ v-if="!isLoading"
+ class="nav-links">
+ <li :class="{ active: scope === null || scope === 'available' }">
+ <a :href="projectEnvironmentsPath">
+ Available
+ <span class="badge js-available-environments-count">
+ {{state.availableCounter}}
+ </span>
+ </a>
+ </li>
+ <li :class="{ active : scope === 'stopped' }">
+ <a :href="projectStoppedEnvironmentsPath">
+ Stopped
+ <span class="badge js-stopped-environments-count">
+ {{state.stoppedCounter}}
+ </span>
</a>
- </div>
+ </li>
+ </ul>
+ <div
+ v-if="canCreateEnvironmentParsed && !isLoading"
+ class="nav-controls">
+ <a
+ :href="newEnvironmentPath"
+ class="btn btn-create">
+ New environment
+ </a>
</div>
+ </div>
+
+ <div class="content-list environments-container">
+ <div
+ class="environments-list-loading text-center"
+ v-if="isLoading">
- <div class="content-list environments-container">
- <div class="environments-list-loading text-center" v-if="isLoading">
- <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
- </div>
-
- <div class="blank-state blank-state-no-icon"
- v-if="!isLoading && state.environments.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- You don't have any environments right now.
- </h2>
- <p class="blank-state-text">
- Environments are places where code gets deployed, such as staging or production.
- <br />
- <a :href="helpPagePath">
- Read more about environments
- </a>
- </p>
-
- <a v-if="canCreateEnvironmentParsed"
- :href="newEnvironmentPath"
- class="btn btn-create js-new-environment-button">
- New Environment
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ </div>
+
+ <div
+ class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.environments.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ You don't have any environments right now.
+ </h2>
+ <p class="blank-state-text">
+ Environments are places where code gets deployed, such as staging or production.
+ <br />
+ <a :href="helpPagePath">
+ Read more about environments
</a>
- </div>
-
- <div class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
-
- <environment-table
- :environments="state.environments"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- :service="service"
- :is-loading-folder-content="isLoadingFolderContent" />
- </div>
-
- <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
- :change="changePage"
- :pageInfo="state.paginationInformation">
- </table-pagination>
+ </p>
+
+ <a
+ v-if="canCreateEnvironmentParsed"
+ :href="newEnvironmentPath"
+ class="btn btn-create js-new-environment-button">
+ New Environment
+ </a>
</div>
+
+ <div
+ class="table-holder"
+ v-if="!isLoading && state.environments.length > 0">
+
+ <environment-table
+ :environments="state.environments"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :service="service"
+ :is-loading-folder-content="isLoadingFolderContent" />
+ </div>
+
+ <table-pagination
+ v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
+ :change="changePage"
+ :pageInfo="state.paginationInformation" />
</div>
- `,
-});
+ </div>
+</template>
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/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js
index 8d963b335cf..c0662125f28 100644
--- a/app/assets/javascripts/environments/environments_bundle.js
+++ b/app/assets/javascripts/environments/environments_bundle.js
@@ -1,13 +1,10 @@
-import EnvironmentsComponent from './components/environment';
+import Vue from 'vue';
+import EnvironmentsComponent from './components/environment.vue';
-$(() => {
- window.gl = window.gl || {};
-
- if (gl.EnvironmentsListApp) {
- gl.EnvironmentsListApp.$destroy(true);
- }
-
- gl.EnvironmentsListApp = new EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- });
-});
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#environments-list-view',
+ components: {
+ 'environments-table-app': EnvironmentsComponent,
+ },
+ render: createElement => createElement('environments-table-app'),
+}));
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index f939eccf246..9add8c3d721 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,13 +1,10 @@
-import EnvironmentsFolderComponent from './environments_folder_view';
+import Vue from 'vue';
+import EnvironmentsFolderComponent from './environments_folder_view.vue';
-$(() => {
- window.gl = window.gl || {};
-
- if (gl.EnvironmentsListFolderApp) {
- gl.EnvironmentsListFolderApp.$destroy(true);
- }
-
- gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
- el: document.querySelector('#environments-folder-list-view'),
- });
-});
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#environments-folder-list-view',
+ components: {
+ 'environments-folder-app': EnvironmentsFolderComponent,
+ },
+ render: createElement => createElement('environments-folder-app'),
+}));
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index d2514593e3a..d27b2acfcdf 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,14 +1,14 @@
+<script>
/* eslint-disable no-new */
/* 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';
import '../../vue_shared/vue_resource_interceptor';
-export default Vue.component('environment-folder-view', {
+export default {
components: {
'environment-table': EnvironmentTable,
'table-pagination': TablePaginationComponent,
@@ -116,54 +116,66 @@ export default Vue.component('environment-folder-view', {
return param;
},
},
+};
+</script>
+<template>
+ <div :class="cssContainerClass">
+ <div
+ class="top-area"
+ v-if="!isLoading">
+
+ <h4 class="js-folder-name environments-folder-name">
+ Environments / <b>{{folderName}}</b>
+ </h4>
+
+ <ul class="nav-links">
+ <li :class="{ active: scope === null || scope === 'available' }">
+ <a
+ :href="availablePath"
+ class="js-available-environments-folder-tab">
+ Available
+ <span class="badge js-available-environments-count">
+ {{state.availableCounter}}
+ </span>
+ </a>
+ </li>
+ <li :class="{ active : scope === 'stopped' }">
+ <a
+ :href="stoppedPath"
+ class="js-stopped-environments-folder-tab">
+ Stopped
+ <span class="badge js-stopped-environments-count">
+ {{state.stoppedCounter}}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
- template: `
- <div :class="cssContainerClass">
- <div class="top-area" v-if="!isLoading">
-
- <h4 class="js-folder-name environments-folder-name">
- Environments / <b>{{folderName}}</b>
- </h4>
-
- <ul class="nav-links">
- <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
- <a :href="availablePath" class="js-available-environments-folder-tab">
- Available
- <span class="badge js-available-environments-count">
- {{state.availableCounter}}
- </span>
- </a>
- </li>
- <li v-bind:class="{ 'active' : scope === 'stopped' }">
- <a :href="stoppedPath" class="js-stopped-environments-folder-tab">
- Stopped
- <span class="badge js-stopped-environments-count">
- {{state.stoppedCounter}}
- </span>
- </a>
- </li>
- </ul>
+ <div class="environments-container">
+ <div
+ class="environments-list-loading text-center"
+ v-if="isLoading">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"/>
</div>
- <div class="environments-container">
- <div class="environments-list-loading text-center" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>
-
- <div class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
+ <div
+ class="table-holder"
+ v-if="!isLoading && state.environments.length > 0">
- <environment-table
- :environments="state.environments"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- :service="service"/>
+ <environment-table
+ :environments="state.environments"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :service="service"/>
- <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
- :change="changePage"
- :pageInfo="state.paginationInformation"/>
- </div>
+ <table-pagination
+ v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
+ :change="changePage"
+ :pageInfo="state.paginationInformation"/>
</div>
</div>
- `,
-});
+ </div>
+</template>
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..36af0674ac6 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -6,489 +6,511 @@ 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.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.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.removeTokenWrapper = this.removeToken.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('click', this.removeTokenWrapper);
+ 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.removeSelectedTokenKeydownWrapper);
+ 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('click', this.removeTokenWrapper);
+ 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.removeSelectedTokenKeydownWrapper);
+ eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
+ }
- // Reposition dropdown so that it is aligned with cursor
- this.dropdownManager.updateCurrentDropdownOffset();
+ checkForBackspace(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ 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');
+ const removeButtonSelected = e.target.closest('.remove-token');
- if (button) {
- e.preventDefault();
- e.stopPropagation();
- gl.FilteredSearchVisualTokens.selectToken(button);
- }
+ if (!removeButtonSelected && 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');
+ removeToken(e) {
+ const removeButtonSelected = e.target.closest('.remove-token');
- if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
- this.dropdownManager.resetDropdowns();
- }
+ if (removeButtonSelected) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const button = e.target.closest('.selectable');
+ gl.FilteredSearchVisualTokens.selectToken(button, true);
+ this.removeSelectedToken();
}
+ }
- editToken(e) {
- const token = e.target.closest('.js-visual-token');
+ 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 (token) {
- gl.FilteredSearchVisualTokens.editToken(token);
- this.tokenChange();
- }
+ if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ this.dropdownManager.resetDropdowns();
}
+ }
- toggleClearSearchButton() {
- const query = gl.DropdownUtils.getSearchQuery();
- const hidden = 'hidden';
- const hasHidden = this.clearSearchButton.classList.contains(hidden);
+ editToken(e) {
+ const token = e.target.closest('.js-visual-token');
- if (query.length === 0 && !hasHidden) {
- this.clearSearchButton.classList.add(hidden);
- } else if (query.length && hasHidden) {
- this.clearSearchButton.classList.remove(hidden);
- }
+ if (token) {
+ gl.FilteredSearchVisualTokens.editToken(token);
+ this.tokenChange();
}
+ }
- handleInputPlaceholder() {
- const query = gl.DropdownUtils.getSearchQuery();
- const placeholder = 'Search or filter results...';
- const currentPlaceholder = this.filteredSearchInput.placeholder;
+ toggleClearSearchButton() {
+ const query = gl.DropdownUtils.getSearchQuery();
+ const hidden = 'hidden';
+ const hasHidden = this.clearSearchButton.classList.contains(hidden);
- if (query.length === 0 && currentPlaceholder !== placeholder) {
- this.filteredSearchInput.placeholder = placeholder;
- } else if (query.length > 0 && currentPlaceholder !== '') {
- this.filteredSearchInput.placeholder = '';
- }
+ if (query.length === 0 && !hasHidden) {
+ this.clearSearchButton.classList.add(hidden);
+ } else if (query.length && hasHidden) {
+ this.clearSearchButton.classList.remove(hidden);
}
+ }
- removeSelectedToken(e) {
- // 8 = Backspace Key
- // 46 = Delete Key
- if (e.keyCode === 8 || e.keyCode === 46) {
- gl.FilteredSearchVisualTokens.removeSelectedToken();
- this.handleInputPlaceholder();
- this.toggleClearSearchButton();
- }
+ 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 = '';
}
+ }
- onClearSearch(e) {
- e.preventDefault();
- this.clearSearch();
+ removeSelectedTokenKeydown(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ this.removeSelectedToken();
}
+ }
- clearSearch() {
- this.filteredSearchInput.value = '';
+ removeSelectedToken() {
+ gl.FilteredSearchVisualTokens.removeSelectedToken();
+ this.handleInputPlaceholder();
+ this.toggleClearSearchButton();
+ this.dropdownManager.updateCurrentDropdownOffset();
+ }
- const removeElements = [];
+ onClearSearch(e) {
+ e.preventDefault();
+ this.clearSearch();
+ }
- [].forEach.call(this.tokensContainer.children, (t) => {
- if (t.classList.contains('js-visual-token')) {
- removeElements.push(t);
- }
- });
+ clearSearch() {
+ this.filteredSearchInput.value = '';
- removeElements.forEach((el) => {
- el.parentElement.removeChild(el);
- });
+ const removeElements = [];
- this.clearSearchButton.classList.add('hidden');
- this.handleInputPlaceholder();
+ [].forEach.call(this.tokensContainer.children, (t) => {
+ if (t.classList.contains('js-visual-token')) {
+ removeElements.push(t);
+ }
+ });
- this.dropdownManager.resetDropdowns();
+ removeElements.forEach((el) => {
+ el.parentElement.removeChild(el);
+ });
- if (this.isHandledAsync) {
- this.search();
- }
+ this.clearSearchButton.classList.add('hidden');
+ this.handleInputPlaceholder();
+
+ this.dropdownManager.resetDropdowns();
+
+ 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();
+ }
}
+ }
+
+ handleFormSubmit(e) {
+ e.preventDefault();
+ this.search();
+ }
+
+ 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
+ });
+ }
- loadSearchParamsFromURL() {
- const params = gl.utils.getUrlParamsArray();
- const usernameParams = this.getUsernameParams();
- let hasFilteredSearch = false;
+ loadSearchParamsFromURL() {
+ const params = gl.utils.getUrlParamsArray();
+ const usernameParams = this.getUsernameParams();
+ let hasFilteredSearch = false;
- params.forEach((p) => {
- const split = p.split('=');
- const keyParam = decodeURIComponent(split[0]);
- const value = split[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);
+ // 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 ? '"' : '\'';
- }
+ 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/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index a5657fc8720..453ecccc6fc 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -16,11 +16,11 @@ class FilteredSearchVisualTokens {
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
}
- static selectToken(tokenButton) {
+ static selectToken(tokenButton, forceSelection = false) {
const selected = tokenButton.classList.contains('selected');
FilteredSearchVisualTokens.unselectTokens();
- if (!selected) {
+ if (!selected || forceSelection) {
tokenButton.classList.add('selected');
}
}
@@ -38,7 +38,12 @@ class FilteredSearchVisualTokens {
return `
<div class="selectable" role="button">
<div class="name"></div>
- <div class="value"></div>
+ <div class="value-container">
+ <div class="value"></div>
+ <div class="remove-token" role="button">
+ <i class="fa fa-close"></i>
+ </div>
+ </div>
</div>
`;
}
@@ -122,7 +127,8 @@ class FilteredSearchVisualTokens {
if (value) {
const button = lastVisualToken.querySelector('.selectable');
- button.removeChild(value);
+ const valueContainer = lastVisualToken.querySelector('.value-container');
+ button.removeChild(valueContainer);
lastVisualToken.innerHTML = button.innerHTML;
} else {
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index b62b2cec4d8..687a462a0d4 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -3,6 +3,7 @@
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji';
+import glRegexp from '~/lib/utils/regexp';
// Creates the variables for setting up GFM auto-completion
window.gl = window.gl || {};
@@ -127,7 +128,15 @@ window.gl.GfmAutoComplete = {
callbacks: {
sorter: this.DefaultOptions.sorter,
beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter
+ filter: this.DefaultOptions.filter,
+
+ matcher: (flag, subtext) => {
+ const relevantText = subtext.trim().split(/\s/).pop();
+ const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi');
+ const match = regexp.exec(relevantText);
+
+ return match && match.length ? match[1] : null;
+ }
}
});
// Team Members
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_show/issue_title.vue b/app/assets/javascripts/issue_show/issue_title.vue
index ba54178a310..00b0e56030a 100644
--- a/app/assets/javascripts/issue_show/issue_title.vue
+++ b/app/assets/javascripts/issue_show/issue_title.vue
@@ -34,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);
@@ -71,7 +60,17 @@ export default {
},
},
created() {
- this.fetch();
+ if (!Visibility.hidden()) {
+ this.poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
},
};
</script>
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/landing.js b/app/assets/javascripts/landing.js
new file mode 100644
index 00000000000..8c0950ad5d5
--- /dev/null
+++ b/app/assets/javascripts/landing.js
@@ -0,0 +1,37 @@
+import Cookies from 'js-cookie';
+
+class Landing {
+ constructor(landingElement, dismissButton, cookieName) {
+ this.landingElement = landingElement;
+ this.cookieName = cookieName;
+ this.dismissButton = dismissButton;
+ this.eventWrapper = {};
+ }
+
+ toggle() {
+ const isDismissed = this.isDismissed();
+
+ this.landingElement.classList.toggle('hidden', isDismissed);
+ if (!isDismissed) this.addEvents();
+ }
+
+ addEvents() {
+ this.eventWrapper.dismissLanding = this.dismissLanding.bind(this);
+ this.dismissButton.addEventListener('click', this.eventWrapper.dismissLanding);
+ }
+
+ removeEvents() {
+ this.dismissButton.removeEventListener('click', this.eventWrapper.dismissLanding);
+ }
+
+ dismissLanding() {
+ this.landingElement.classList.add('hidden');
+ Cookies.set(this.cookieName, 'true', { expires: 365 });
+ }
+
+ isDismissed() {
+ return Cookies.get(this.cookieName) === 'true';
+ }
+}
+
+export default Landing;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 33c900e264b..8058672eaa9 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -169,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;
@@ -368,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);
}
};
@@ -385,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/regexp.js b/app/assets/javascripts/lib/utils/regexp.js
new file mode 100644
index 00000000000..baa0b51d59b
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/regexp.js
@@ -0,0 +1,10 @@
+/**
+ * Regexp utility for the convenience of working with regular expressions.
+ *
+ */
+
+// Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203
+// Unicode 6.1
+const unicodeLetters = '\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC';
+
+export default { unicodeLetters };
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 1821ca18053..3ac6dedf131 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -41,7 +41,6 @@ require('vendor/jquery.scrollTo');
LineHighlighter.prototype._hash = '';
function LineHighlighter(hash) {
- var range;
if (hash == null) {
// Initialize a LineHighlighter object
//
@@ -51,10 +50,22 @@ require('vendor/jquery.scrollTo');
this.setHash = bind(this.setHash, this);
this.highlightLine = bind(this.highlightLine, this);
this.clickHandler = bind(this.clickHandler, this);
+ this.highlightHash = this.highlightHash.bind(this);
this._hash = hash;
this.bindEvents();
- if (hash !== '') {
- range = this.hashToRange(hash);
+ this.highlightHash();
+ }
+
+ LineHighlighter.prototype.bindEvents = function() {
+ const $fileHolder = $('.file-holder');
+ $fileHolder.on('click', 'a[data-line-number]', this.clickHandler);
+ $fileHolder.on('highlight:line', this.highlightHash);
+ };
+
+ LineHighlighter.prototype.highlightHash = function() {
+ var range;
+ if (this._hash !== '') {
+ range = this.hashToRange(this._hash);
if (range[0]) {
this.highlightRange(range);
$.scrollTo("#L" + range[0], {
@@ -64,10 +75,6 @@ require('vendor/jquery.scrollTo');
});
}
}
- }
-
- LineHighlighter.prototype.bindEvents = function() {
- $('#blob-content-holder').on('click', 'a[data-line-number]', this.clickHandler);
};
LineHighlighter.prototype.clickHandler = function(event) {
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_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index f7f6a773036..93c30c54a8e 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -5,6 +5,7 @@
import Cookies from 'js-cookie';
import './breakpoints';
import './flash';
+import BlobForkSuggestion from './blob/blob_fork_suggestion';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -266,6 +267,17 @@ import './flash';
new gl.Diff();
this.scrollToElement('#diffs');
+
+ $('.diff-file').each((i, el) => {
+ new BlobForkSuggestion({
+ openButtons: $(el).find('.js-edit-blob-link-fork-toggler'),
+ forkButtons: $(el).find('.js-fork-suggestion-button'),
+ cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
+ suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
+ actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
+ })
+ .init();
+ });
},
});
}
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/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/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index 9c58c465001..64c1447f427 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -28,7 +28,9 @@ export default class MiniPipelineGraph {
* All dropdown events are fired at the .dropdown-menu's parent element.
*/
bindEvents() {
- $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList);
+ $(document)
+ .off('shown.bs.dropdown', this.container)
+ .on('shown.bs.dropdown', this.container, this.getBuildsList);
}
/**
@@ -91,6 +93,9 @@ export default class MiniPipelineGraph {
},
error: () => {
this.toggleLoading(button);
+ if ($(button).parent().hasClass('open')) {
+ $(button).dropdown('toggle');
+ }
new Flash('An error occurred while fetching the builds.', 'alert');
},
});
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index d82a4eb9642..78bb0e6fb47 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -22,6 +22,7 @@ class PrometheusGraph {
const hasMetrics = $prometheusContainer.data('has-metrics');
this.docLink = $prometheusContainer.data('doc-link');
this.integrationLink = $prometheusContainer.data('prometheus-integration');
+ this.state = '';
$(document).ajaxError(() => {});
@@ -38,8 +39,9 @@ class PrometheusGraph {
this.configureGraph();
this.init();
} else {
+ const prevState = this.state;
this.state = '.js-getting-started';
- this.updateState();
+ this.updateState(prevState);
}
}
@@ -53,21 +55,23 @@ class PrometheusGraph {
}
init() {
- this.getData().then((metricsResponse) => {
+ return this.getData().then((metricsResponse) => {
let enoughData = true;
- Object.keys(metricsResponse.metrics).forEach((key) => {
- let currentKey;
- if (key === 'cpu_values' || key === 'memory_values') {
- currentKey = metricsResponse.metrics[key];
- if (Object.keys(currentKey).length === 0) {
- enoughData = false;
- }
- }
- });
- if (!enoughData) {
- this.state = '.js-loading';
- this.updateState();
+ if (typeof metricsResponse === 'undefined') {
+ enoughData = false;
} else {
+ Object.keys(metricsResponse.metrics).forEach((key) => {
+ if (key === 'cpu_values' || key === 'memory_values') {
+ const currentData = (metricsResponse.metrics[key])[0];
+ if (currentData.values.length <= 2) {
+ enoughData = false;
+ }
+ }
+ });
+ }
+ if (enoughData) {
+ $(prometheusStatesContainer).hide();
+ $(prometheusParentGraphContainer).show();
this.transformData(metricsResponse);
this.createGraph();
}
@@ -340,6 +344,8 @@ class PrometheusGraph {
getData() {
const maxNumberOfRequests = 3;
+ this.state = '.js-loading';
+ this.updateState();
return gl.utils.backOff((next, stop) => {
$.ajax({
url: metricsEndpoint,
@@ -350,12 +356,11 @@ class PrometheusGraph {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < maxNumberOfRequests) {
next();
- } else {
- stop({
- status: resp.status,
- metrics: data,
- });
+ } else if (this.backOffRequestCounter >= maxNumberOfRequests) {
+ stop(new Error('loading'));
}
+ } else if (!data.success) {
+ stop(new Error('loading'));
} else {
stop({
status: resp.status,
@@ -371,8 +376,9 @@ class PrometheusGraph {
return resp.metrics;
})
.catch(() => {
+ const prevState = this.state;
this.state = '.js-unable-to-connect';
- this.updateState();
+ this.updateState(prevState);
});
}
@@ -380,19 +386,20 @@ class PrometheusGraph {
Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0];
- if (metricValues !== undefined) {
- this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
- time: new Date(metric[0] * 1000),
- value: metric[1],
- }));
- }
+ this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
+ time: new Date(metric[0] * 1000),
+ value: metric[1],
+ }));
}
});
}
- updateState() {
+ updateState(prevState) {
const $statesContainer = $(prometheusStatesContainer);
$(prometheusParentGraphContainer).hide();
+ if (prevState) {
+ $(`${prevState}`, $statesContainer).addClass('hidden');
+ }
$(`${this.state}`, $statesContainer).removeClass('hidden');
$(prometheusStatesContainer).show();
}
diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue
new file mode 100644
index 00000000000..b8a16356576
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/code.vue
@@ -0,0 +1,58 @@
+<template>
+ <div class="cell">
+ <code-cell
+ type="input"
+ :raw-code="rawInputCode"
+ :count="cell.execution_count"
+ :code-css-class="codeCssClass" />
+ <output-cell
+ v-if="hasOutput"
+ :count="cell.execution_count"
+ :output="output"
+ :code-css-class="codeCssClass" />
+ </div>
+</template>
+
+<script>
+import CodeCell from './code/index.vue';
+import OutputCell from './output/index.vue';
+
+export default {
+ components: {
+ 'code-cell': CodeCell,
+ 'output-cell': OutputCell,
+ },
+ props: {
+ cell: {
+ type: Object,
+ required: true,
+ },
+ codeCssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ rawInputCode() {
+ if (this.cell.source) {
+ return this.cell.source.join('');
+ }
+
+ return '';
+ },
+ hasOutput() {
+ return this.cell.outputs.length;
+ },
+ output() {
+ return this.cell.outputs[0];
+ },
+ },
+};
+</script>
+
+<style scoped>
+.cell {
+ flex-direction: column;
+}
+</style>
diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue
new file mode 100644
index 00000000000..31b30f601e2
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/code/index.vue
@@ -0,0 +1,57 @@
+<template>
+ <div :class="type">
+ <prompt
+ :type="promptType"
+ :count="count" />
+ <pre
+ class="language-python"
+ :class="codeCssClass"
+ ref="code"
+ v-text="code">
+ </pre>
+ </div>
+</template>
+
+<script>
+ import Prism from '../../lib/highlight';
+ import Prompt from '../prompt.vue';
+
+ export default {
+ components: {
+ prompt: Prompt,
+ },
+ props: {
+ count: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ codeCssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ rawCode: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ code() {
+ return this.rawCode;
+ },
+ promptType() {
+ const type = this.type.split('put')[0];
+
+ return type.charAt(0).toUpperCase() + type.slice(1);
+ },
+ },
+ mounted() {
+ Prism.highlightElement(this.$refs.code);
+ },
+ };
+</script>
diff --git a/app/assets/javascripts/notebook/cells/index.js b/app/assets/javascripts/notebook/cells/index.js
new file mode 100644
index 00000000000..e4c255609fe
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/index.js
@@ -0,0 +1,2 @@
+export { default as MarkdownCell } from './markdown.vue';
+export { default as CodeCell } from './code.vue';
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
new file mode 100644
index 00000000000..3e8240d10ec
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -0,0 +1,98 @@
+<template>
+ <div class="cell text-cell">
+ <prompt />
+ <div class="markdown" v-html="markdown"></div>
+ </div>
+</template>
+
+<script>
+ /* global katex */
+ import marked from 'marked';
+ import Prompt from './prompt.vue';
+
+ const renderer = new marked.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
+ */
+ const katexRegexString = `(
+ ^\\\\begin{[a-zA-Z]+}\\s
+ |
+ ^\\$\\$
+ |
+ \\s\\$(?!\\$)
+ )
+ (.+?)
+ (
+ \\s\\\\end{[a-zA-Z]+}$
+ |
+ \\$\\$$
+ |
+ \\$
+ )
+ `.replace(/\s/g, '').trim();
+
+ renderer.paragraph = (t) => {
+ let text = t;
+ let inline = false;
+
+ if (typeof katex !== 'undefined') {
+ const katexString = text.replace(/\\/g, '\\');
+ const 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>`;
+ };
+
+ marked.setOptions({
+ sanitize: true,
+ renderer,
+ });
+
+ export default {
+ components: {
+ prompt: Prompt,
+ },
+ props: {
+ cell: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ markdown() {
+ return marked(this.cell.source.join(''));
+ },
+ },
+ };
+</script>
+
+<style>
+.markdown .katex {
+ display: block;
+ text-align: center;
+}
+
+.markdown .inline-katex .katex {
+ display: inline;
+ text-align: initial;
+}
+</style>
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
new file mode 100644
index 00000000000..0f39cd138df
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -0,0 +1,22 @@
+<template>
+ <div class="output">
+ <prompt />
+ <div v-html="rawCode"></div>
+ </div>
+</template>
+
+<script>
+import Prompt from '../prompt.vue';
+
+export default {
+ props: {
+ rawCode: {
+ type: String,
+ required: true,
+ },
+ },
+ components: {
+ prompt: Prompt,
+ },
+};
+</script>
diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue
new file mode 100644
index 00000000000..f3b873bbc0f
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/output/image.vue
@@ -0,0 +1,27 @@
+<template>
+ <div class="output">
+ <prompt />
+ <img
+ :src="'data:' + outputType + ';base64,' + rawCode" />
+ </div>
+</template>
+
+<script>
+import Prompt from '../prompt.vue';
+
+export default {
+ props: {
+ outputType: {
+ type: String,
+ required: true,
+ },
+ rawCode: {
+ type: String,
+ required: true,
+ },
+ },
+ components: {
+ prompt: Prompt,
+ },
+};
+</script>
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
new file mode 100644
index 00000000000..23c9ea78939
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -0,0 +1,83 @@
+<template>
+ <component :is="componentName"
+ type="output"
+ :outputType="outputType"
+ :count="count"
+ :raw-code="rawCode"
+ :code-css-class="codeCssClass" />
+</template>
+
+<script>
+import CodeCell from '../code/index.vue';
+import Html from './html.vue';
+import Image from './image.vue';
+
+export default {
+ props: {
+ codeCssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ count: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ output: {
+ type: Object,
+ requred: true,
+ },
+ },
+ components: {
+ 'code-cell': CodeCell,
+ 'html-output': Html,
+ 'image-output': Image,
+ },
+ data() {
+ return {
+ outputType: '',
+ };
+ },
+ computed: {
+ componentName() {
+ if (this.output.text) {
+ return 'code-cell';
+ } else if (this.output.data['image/png']) {
+ this.outputType = 'image/png';
+
+ return 'image-output';
+ } else if (this.output.data['text/html']) {
+ this.outputType = 'text/html';
+
+ return 'html-output';
+ } else if (this.output.data['image/svg+xml']) {
+ this.outputType = 'image/svg+xml';
+
+ return 'html-output';
+ }
+
+ this.outputType = 'text/plain';
+ return 'code-cell';
+ },
+ rawCode() {
+ if (this.output.text) {
+ return this.output.text.join('');
+ }
+
+ return this.dataForType(this.outputType);
+ },
+ },
+ methods: {
+ dataForType(type) {
+ let data = this.output.data[type];
+
+ if (typeof data === 'object') {
+ data = data.join('');
+ }
+
+ return data;
+ },
+ },
+};
+</script>
diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue
new file mode 100644
index 00000000000..4540e4248d8
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/prompt.vue
@@ -0,0 +1,30 @@
+<template>
+ <div class="prompt">
+ <span v-if="type && count">
+ {{ type }} [{{ count }}]:
+ </span>
+ </div>
+</template>
+
+<script>
+ export default {
+ props: {
+ type: {
+ type: String,
+ required: false,
+ },
+ count: {
+ type: Number,
+ required: false,
+ },
+ },
+ };
+</script>
+
+<style scoped>
+.prompt {
+ padding: 0 10px;
+ min-width: 7em;
+ font-family: monospace;
+}
+</style>
diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue
new file mode 100644
index 00000000000..fd62c1231ef
--- /dev/null
+++ b/app/assets/javascripts/notebook/index.vue
@@ -0,0 +1,75 @@
+<template>
+ <div v-if="hasNotebook">
+ <component
+ v-for="(cell, index) in cells"
+ :is="cellType(cell.cell_type)"
+ :cell="cell"
+ :key="index"
+ :code-css-class="codeCssClass" />
+ </div>
+</template>
+
+<script>
+ import {
+ MarkdownCell,
+ CodeCell,
+ } from './cells';
+
+ export default {
+ components: {
+ 'code-cell': CodeCell,
+ 'markdown-cell': MarkdownCell,
+ },
+ props: {
+ notebook: {
+ type: Object,
+ required: true,
+ },
+ codeCssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ methods: {
+ cellType(type) {
+ return `${type}-cell`;
+ },
+ },
+ computed: {
+ cells() {
+ if (this.notebook.worksheets) {
+ const data = {
+ cells: [],
+ };
+
+ return this.notebook.worksheets.reduce((cellData, sheet) => {
+ const cellDataCopy = cellData;
+ cellDataCopy.cells = cellDataCopy.cells.concat(sheet.cells);
+ return cellDataCopy;
+ }, data).cells;
+ }
+
+ return this.notebook.cells;
+ },
+ hasNotebook() {
+ return Object.keys(this.notebook).length;
+ },
+ },
+ };
+</script>
+
+<style>
+.cell,
+.input,
+.output {
+ display: flex;
+ width: 100%;
+ margin-bottom: 10px;
+}
+
+.cell pre {
+ margin: 0;
+ width: 100%;
+}
+</style>
diff --git a/app/assets/javascripts/notebook/lib/highlight.js b/app/assets/javascripts/notebook/lib/highlight.js
new file mode 100644
index 00000000000..74ade6d2edf
--- /dev/null
+++ b/app/assets/javascripts/notebook/lib/highlight.js
@@ -0,0 +1,22 @@
+import Prism from 'prismjs';
+import 'prismjs/components/prism-python';
+import 'prismjs/plugins/custom-class/prism-custom-class';
+
+Prism.plugins.customClass.map({
+ comment: 'c',
+ error: 'err',
+ operator: 'o',
+ constant: 'kc',
+ namespace: 'kn',
+ keyword: 'k',
+ string: 's',
+ number: 'm',
+ 'attr-name': 'na',
+ builtin: 'nb',
+ entity: 'ni',
+ function: 'nf',
+ tag: 'nt',
+ variable: 'nv',
+});
+
+export default Prism;
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/pdf/assets/img/bg.gif b/app/assets/javascripts/pdf/assets/img/bg.gif
new file mode 100644
index 00000000000..c7e98e044f5
--- /dev/null
+++ b/app/assets/javascripts/pdf/assets/img/bg.gif
Binary files differ
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
new file mode 100644
index 00000000000..4603859d7b0
--- /dev/null
+++ b/app/assets/javascripts/pdf/index.vue
@@ -0,0 +1,73 @@
+<template>
+ <div class="pdf-viewer" v-if="hasPDF">
+ <page v-for="(page, index) in pages"
+ :key="index"
+ :v-if="!loading"
+ :page="page"
+ :number="index + 1" />
+ </div>
+</template>
+
+<script>
+ import pdfjsLib from 'pdfjs-dist';
+ import workerSrc from 'vendor/pdf.worker';
+
+ import page from './page/index.vue';
+
+ export default {
+ props: {
+ pdf: {
+ type: [String, Uint8Array],
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ pages: [],
+ };
+ },
+ components: { page },
+ watch: { pdf: 'load' },
+ computed: {
+ document() {
+ return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf };
+ },
+ hasPDF() {
+ return this.pdf && this.pdf.length > 0;
+ },
+ },
+ methods: {
+ load() {
+ this.pages = [];
+ return pdfjsLib.getDocument(this.document)
+ .then(this.renderPages)
+ .then(() => this.$emit('pdflabload'))
+ .catch(error => this.$emit('pdflaberror', error))
+ .then(() => { this.loading = false; });
+ },
+ renderPages(pdf) {
+ const pagePromises = [];
+ this.loading = true;
+ for (let num = 1; num <= pdf.numPages; num += 1) {
+ pagePromises.push(
+ pdf.getPage(num).then(p => this.pages.push(p)),
+ );
+ }
+ return Promise.all(pagePromises);
+ },
+ },
+ mounted() {
+ pdfjsLib.PDFJS.workerSrc = workerSrc;
+ if (this.hasPDF) this.load();
+ },
+ };
+</script>
+
+<style>
+ .pdf-viewer {
+ background: url('./assets/img/bg.gif');
+ display: flex;
+ flex-flow: column nowrap;
+ }
+</style>
diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue
new file mode 100644
index 00000000000..7b74ee4eb2e
--- /dev/null
+++ b/app/assets/javascripts/pdf/page/index.vue
@@ -0,0 +1,68 @@
+<template>
+ <canvas
+ class="pdf-page"
+ ref="canvas"
+ :data-page="number" />
+</template>
+
+<script>
+ export default {
+ props: {
+ page: {
+ type: Object,
+ required: true,
+ },
+ number: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scale: 4,
+ rendering: false,
+ };
+ },
+ computed: {
+ viewport() {
+ return this.page.getViewport(this.scale);
+ },
+ context() {
+ return this.$refs.canvas.getContext('2d');
+ },
+ renderContext() {
+ return {
+ canvasContext: this.context,
+ viewport: this.viewport,
+ };
+ },
+ },
+ mounted() {
+ this.$refs.canvas.height = this.viewport.height;
+ this.$refs.canvas.width = this.viewport.width;
+ this.rendering = true;
+ this.page.render(this.renderContext)
+ .then(() => { this.rendering = false; })
+ .catch(error => this.$emit('pdflaberror', error));
+ },
+ };
+</script>
+
+<style>
+.pdf-page {
+ margin: 8px auto 0 auto;
+ border-top: 1px #ddd solid;
+ border-bottom: 1px #ddd solid;
+ width: 100%;
+}
+
+.pdf-page:first-child {
+ margin-top: 0px;
+ border-top: 0px;
+}
+
+.pdf-page:last-child {
+ margin-bottom: 0px;
+ border-bottom: 0px;
+}
+</style>
diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue
index 11da6e908b7..d1c60b570de 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/async_button.vue
+++ b/app/assets/javascripts/pipelines/components/async_button.vue
@@ -65,6 +65,8 @@ export default {
makeRequest() {
this.isLoading = true;
+ $(this.$el).tooltip('destroy');
+
this.service.postAction(this.endpoint)
.then(() => {
this.isLoading = false;
@@ -88,9 +90,13 @@ export default {
:aria-label="title"
data-container="body"
data-placement="top"
- :disabled="isLoading"
- >
- <i :class="iconClass" aria-hidden="true"></i>
- <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading"></i>
+ :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/vue_pipelines_index/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index ba158bc4a1e..3db64339a62 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -13,7 +13,7 @@ export default {
</script>
<template>
- <div class="row empty-state">
+ <div class="row empty-state js-empty-state">
<div class="col-xs-12">
<div class="svg-content" v-html="pipelinesEmptyStateSVG" />
</div>
diff --git a/app/assets/javascripts/vue_pipelines_index/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue
index 90cee68163e..90cee68163e 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/error_state.vue
+++ b/app/assets/javascripts/pipelines/components/error_state.vue
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..203485f2990 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/stage.js
+++ b/app/assets/javascripts/pipelines/components/stage.js
@@ -1,35 +1,7 @@
/* 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],
- };
- },
-
props: {
stage: {
type: Object,
@@ -37,6 +9,13 @@ export default {
},
},
+ data() {
+ return {
+ builds: '',
+ spinner: '<span class="fa fa-spinner fa-spin"></span>',
+ };
+ },
+
updated() {
if (this.builds) {
this.stopDropdownClickPropagation();
@@ -52,7 +31,13 @@ export default {
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.builds = JSON.parse(response.body).html;
- }, () => {
+ })
+ .catch(() => {
+ // If dropdown is opened we'll close it.
+ if (this.$el.classList.contains('open')) {
+ $(this.$refs.dropdown).dropdown('toggle');
+ }
+
const flash = new Flash('Something went wrong on our end.');
return flash;
});
@@ -67,9 +52,10 @@ export default {
* target the click event of this component.
*/
stopDropdownClickPropagation() {
- $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
- e.stopPropagation();
- });
+ $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
+ .on('click', (e) => {
+ e.stopPropagation();
+ });
},
},
computed: {
@@ -89,6 +75,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>
@@ -99,12 +88,22 @@ export default {
data-placement="top"
data-toggle="dropdown"
type="button"
- :aria-label="stage.title">
- <span v-html="svg" aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
+ :aria-label="stage.title"
+ ref="dropdown">
+ <span
+ v-html="svgHTML"
+ aria-hidden="true">
+ </span>
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true" />
</button>
- <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
- <div class="arrow-up" aria-hidden="true"></div>
+ <ul
+ ref="dropdown-content"
+ class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+ <div
+ class="arrow-up"
+ aria-hidden="true"></div>
<div
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
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/pipelines/components/time_ago.js b/app/assets/javascripts/pipelines/components/time_ago.js
new file mode 100644
index 00000000000..188f74cc705
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/time_ago.js
@@ -0,0 +1,98 @@
+import iconTimerSvg from 'icons/_icon_timer.svg';
+import '../../lib/utils/datetime_utility';
+
+export default {
+ props: {
+ finishedTime: {
+ type: String,
+ required: true,
+ },
+
+ duration: {
+ type: Number,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ iconTimerSvg,
+ };
+ },
+
+ updated() {
+ $(this.$refs.tooltip).tooltip('fixTitle');
+ },
+
+ computed: {
+ hasDuration() {
+ return this.duration > 0;
+ },
+
+ hasFinishedTime() {
+ return this.finishedTime !== '';
+ },
+
+ localTimeFinished() {
+ return gl.utils.formatDate(this.finishedTime);
+ },
+
+ durationFormated() {
+ const date = new Date(this.duration * 1000);
+
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
+
+ // left pad
+ if (hh < 10) {
+ hh = `0${hh}`;
+ }
+ if (mm < 10) {
+ mm = `0${mm}`;
+ }
+ if (ss < 10) {
+ ss = `0${ss}`;
+ }
+
+ return `${hh}:${mm}:${ss}`;
+ },
+
+ finishedTimeFormated() {
+ const timeAgo = gl.utils.getTimeago();
+
+ return timeAgo.format(this.finishedTime);
+ },
+ },
+
+ template: `
+ <td class="pipelines-time-ago">
+ <p
+ class="duration"
+ v-if="hasDuration">
+ <span
+ v-html="iconTimerSvg">
+ </span>
+ {{durationFormated}}
+ </p>
+
+ <p
+ class="finished-at"
+ v-if="hasFinishedTime">
+
+ <i
+ class="fa fa-calendar"
+ aria-hidden="true" />
+
+ <time
+ ref="tooltip"
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body"
+ :title="localTimeFinished">
+ {{finishedTimeFormated}}
+ </time>
+ </p>
+ </td>
+ `,
+};
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 6eea4812f33..93d4818231f 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js
+++ b/app/assets/javascripts/pipelines/pipelines.js
@@ -1,4 +1,3 @@
-import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
@@ -161,15 +160,6 @@ export default {
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
- beforeUpdate() {
- if (this.state.pipelines.length &&
- this.$children &&
- !this.isMakingRequest &&
- !this.isLoading) {
- this.store.startTimeAgoLoops.call(this, Vue);
- }
- },
-
beforeDestroyed() {
eventHub.$off('refreshPipelines');
},
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/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js
new file mode 100644
index 00000000000..ffefe0192f2
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js
@@ -0,0 +1,30 @@
+export default class PipelinesStore {
+ constructor() {
+ this.state = {};
+
+ this.state.pipelines = [];
+ this.state.count = {};
+ this.state.pageInfo = {};
+ }
+
+ storePipelines(pipelines = []) {
+ this.state.pipelines = pipelines;
+ }
+
+ storeCount(count = {}) {
+ this.state.count = count;
+ }
+
+ storePagination(pagination = {}) {
+ let paginationInfo;
+
+ if (Object.keys(pagination).length) {
+ const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+ paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+ } else {
+ paginationInfo = pagination;
+ }
+
+ this.state.pageInfo = paginationInfo;
+ }
+}
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_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..0344ce9ffb4 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -33,6 +33,7 @@
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id');
+ options.groupId = $dropdown.data('group-id');
options.showCurrentUser = $dropdown.data('current-user');
options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter');
@@ -56,6 +57,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/time_ago.js b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
deleted file mode 100644
index 498d0715f54..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import iconTimerSvg from 'icons/_icon_timer.svg';
-import '../../lib/utils/datetime_utility';
-
-export default {
- data() {
- return {
- currentTime: new Date(),
- iconTimerSvg,
- };
- },
- props: ['pipeline'],
- computed: {
- timeAgo() {
- return gl.utils.getTimeago();
- },
- localTimeFinished() {
- return gl.utils.formatDate(this.pipeline.details.finished_at);
- },
- timeStopped() {
- const changeTime = this.currentTime;
- const options = {
- weekday: 'long',
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- };
- options.timeZoneName = 'short';
- const finished = this.pipeline.details.finished_at;
- if (!finished && changeTime) return false;
- return ({ words: this.timeAgo.format(finished) });
- },
- duration() {
- const { duration } = this.pipeline.details;
- const date = new Date(duration * 1000);
-
- let hh = date.getUTCHours();
- let mm = date.getUTCMinutes();
- let ss = date.getSeconds();
-
- if (hh < 10) hh = `0${hh}`;
- if (mm < 10) mm = `0${mm}`;
- if (ss < 10) ss = `0${ss}`;
-
- if (duration !== null) return `${hh}:${mm}:${ss}`;
- return false;
- },
- },
- methods: {
- changeTime() {
- this.currentTime = new Date();
- },
- },
- template: `
- <td class="pipelines-time-ago">
- <p class="duration" v-if='duration'>
- <span v-html="iconTimerSvg"></span>
- {{duration}}
- </p>
- <p class="finished-at" v-if='timeStopped'>
- <i class="fa fa-calendar"></i>
- <time
- data-toggle="tooltip"
- data-placement="top"
- data-container="body"
- :data-original-title='localTimeFinished'>
- {{timeStopped.words}}
- </time>
- </p>
- </td>
- `,
-};
diff --git a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
deleted file mode 100644
index 377ec8ba2cc..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/* eslint-disable no-underscore-dangle*/
-import VueRealtimeListener from '../../vue_realtime_listener';
-
-export default class PipelinesStore {
- constructor() {
- this.state = {};
-
- this.state.pipelines = [];
- this.state.count = {};
- this.state.pageInfo = {};
- }
-
- storePipelines(pipelines = []) {
- this.state.pipelines = pipelines;
- }
-
- storeCount(count = {}) {
- this.state.count = count;
- }
-
- storePagination(pagination = {}) {
- let paginationInfo;
-
- if (Object.keys(pagination).length) {
- const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
- paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
- } else {
- paginationInfo = pagination;
- }
-
- this.state.pageInfo = paginationInfo;
- }
-
- /**
- * FIXME: Move this inside the component.
- *
- * Once the data is received we will start the time ago loops.
- *
- * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
- * update the time to show how long as passed.
- *
- */
- startTimeAgoLoops() {
- const startTimeLoops = () => {
- this.timeLoopInterval = setInterval(() => {
- this.$children[0].$children.reduce((acc, component) => {
- const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
- acc.push(timeAgoComponent);
- return acc;
- }, []).forEach(e => e.changeTime());
- }, 10000);
- };
-
- startTimeLoops();
-
- const removeIntervals = () => clearInterval(this.timeLoopInterval);
- const startIntervals = () => startTimeLoops();
-
- VueRealtimeListener(removeIntervals, startIntervals);
- }
-}
diff --git a/app/assets/javascripts/vue_realtime_listener/index.js b/app/assets/javascripts/vue_realtime_listener/index.js
deleted file mode 100644
index 4ddb2f975b0..00000000000
--- a/app/assets/javascripts/vue_realtime_listener/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export default (removeIntervals, startIntervals) => {
- window.removeEventListener('focus', startIntervals);
- window.removeEventListener('blur', removeIntervals);
- window.removeEventListener('onbeforeload', removeIntervals);
-
- window.addEventListener('focus', startIntervals);
- window.addEventListener('blur', removeIntervals);
- window.addEventListener('onbeforeload', removeIntervals);
-};
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 8ebe12cb1c5..79806bc7204 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,11 @@
/* eslint-disable no-param-reassign */
-
-import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button.vue';
-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';
/**
@@ -166,6 +165,32 @@ export default {
}
return undefined;
},
+
+ /**
+ * Timeago components expects a number
+ *
+ * @return {type} description
+ */
+ pipelineDuration() {
+ if (this.pipeline.details && this.pipeline.details.duration) {
+ return this.pipeline.details.duration;
+ }
+
+ return 0;
+ },
+
+ /**
+ * Timeago component expects a String.
+ *
+ * @return {String}
+ */
+ pipelineFinishedAt() {
+ if (this.pipeline.details && this.pipeline.details.finished_at) {
+ return this.pipeline.details.finished_at;
+ }
+
+ return '';
+ },
},
template: `
@@ -192,7 +217,9 @@ export default {
</div>
</td>
- <time-ago :pipeline="pipeline"/>
+ <time-ago
+ :duration="pipelineDuration"
+ :finished-time="pipelineFinishedAt" />
<td class="pipeline-actions">
<div class="pull-right btn-group">
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 f614f262316..9159927ed8b 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -108,8 +108,7 @@
}
.award-control {
- margin: 3px 5px 3px 0;
- padding: .35em .4em;
+ margin-right: 5px;
outline: 0;
&.disabled {
@@ -228,8 +227,8 @@
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
- left: 7px;
- bottom: 9px;
+ left: 11px;
+ bottom: 7px;
opacity: 0;
@include transition(opacity, transform);
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 52425262925..ac1fc0eb8ae 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;
}
}
@@ -255,6 +254,63 @@
padding: 10px 0;
}
+.landing {
+ margin-bottom: $gl-padding;
+ overflow: hidden;
+ display: flex;
+ position: relative;
+ border: 1px solid $blue-300;
+ border-radius: $border-radius-default;
+ background-color: $blue-25;
+ justify-content: center;
+
+ .dismiss-button {
+ position: absolute;
+ right: 6px;
+ top: 6px;
+ cursor: pointer;
+ color: $blue-300;
+ z-index: 1;
+ border: none;
+ background-color: transparent;
+
+ &:hover,
+ &:focus {
+ border: none;
+ color: $blue-400;
+ }
+ }
+
+ .svg-container {
+ align-self: center;
+ }
+
+ .inner-content {
+ text-align: left;
+ white-space: nowrap;
+
+ h4 {
+ color: $gl-text-color;
+ font-size: 17px;
+ }
+
+ p {
+ color: $gl-text-color;
+ margin-bottom: $gl-padding;
+ }
+ }
+
+ @media (max-width: $screen-sm-min) {
+ flex-direction: column;
+
+ .inner-content {
+ white-space: normal;
+ padding: 0 28px;
+ text-align: center;
+ }
+ }
+}
+
.empty-state {
margin: 100px 0 0;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 2c33b235980..1a6f36d032d 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;
}
@@ -66,7 +70,7 @@ pre {
}
hr {
- margin: $gl-padding 0;
+ margin: 24px 0;
border-top: 1px solid darken($gray-normal, 8%);
}
@@ -420,6 +424,11 @@ table {
}
}
+.bordered-box {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+}
+
.str-truncated {
&-60 {
@include str-truncated(60%);
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 7767826b033..1313ea25c2a 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,11 @@
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
+ @include set-invisible;
+
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
&.is-loading {
.dropdown-content {
@@ -252,6 +276,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 +367,10 @@
.dropdown-select {
width: $dropdown-width;
+
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
}
.dropdown-menu-align-right {
@@ -564,3 +609,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/files.scss b/app/assets/stylesheets/framework/files.scss
index a5a8522739e..c197bf6b9f5 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -61,11 +61,13 @@
.file-content {
background: $white-light;
- &.image_file {
+ &.image_file,
+ &.video {
background: $file-image-bg;
text-align: center;
- img {
+ img,
+ video {
padding: 20px;
max-width: 80%;
}
@@ -73,14 +75,6 @@
&.wiki {
padding: 30px $gl-padding;
-
- .highlight {
- margin-bottom: 9px;
-
- > pre {
- margin: 0;
- }
- }
}
&.blob-no-preview {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 11d44df4867..0692f65043b 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -104,6 +104,24 @@
padding: 2px 7px;
}
+ .value {
+ padding-right: 0;
+ }
+
+ .remove-token {
+ display: inline-block;
+ padding-left: 4px;
+ padding-right: 8px;
+
+ .fa-close {
+ color: $gl-text-color-disabled;
+ }
+
+ &:hover .fa-close {
+ color: $gl-text-color-secondary;
+ }
+ }
+
.name {
background-color: $filter-name-resting-color;
color: $filter-name-text-color;
@@ -112,7 +130,7 @@
text-transform: capitalize;
}
- .value {
+ .value-container {
background-color: $white-normal;
color: $filter-value-text-color;
border-radius: 0 2px 2px 0;
@@ -124,7 +142,7 @@
background-color: $filter-name-selected-color;
}
- .value {
+ .value-container {
background-color: $filter-value-selected-color;
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 2071d4dcfce..6d9218310eb 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -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/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index a668a6c4c39..80691a234f8 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -120,6 +120,10 @@
// Ensure that image does not exceed viewport
max-height: calc(100vh - 100px);
}
+
+ table {
+ @include markdown-table;
+ }
}
.toolbar-group {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index b3340d41333..3a98332e46c 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -13,6 +13,13 @@
}
/*
+ * Mixin for markdown tables
+ */
+@mixin markdown-table {
+ width: auto;
+}
+
+/*
* Base mixin for lists in GitLab
*/
@mixin basic-list {
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/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index cd23deb6d75..d2164a1d333 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -4,7 +4,7 @@
padding: 0;
.timeline-entry {
- padding: $gl-padding $gl-btn-padding 14px;
+ padding: $gl-padding $gl-btn-padding 0;
border-color: $white-normal;
color: $gl-text-color;
border-bottom: 1px solid $border-white-light;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 664539e93e1..96d8a812723 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -8,6 +8,13 @@
img {
max-width: 100%;
+ margin: 0 0 8px;
+ }
+
+ p a:not(.no-attachment-icon) img {
+ // Remove bottom padding because
+ // <p> already has $gl-padding bottom
+ margin-bottom: 0;
}
*:first-child:not(.katex-display) {
@@ -47,44 +54,50 @@
h1 {
font-size: 1.75em;
font-weight: 600;
- margin: 16px 0 10px;
- padding: 0 0 0.3em;
+ margin: 24px 0 16px;
+ padding-bottom: 0.3em;
border-bottom: 1px solid $white-dark;
color: $gl-text-color;
+
+ &:first-child {
+ margin-top: 0;
+ }
}
h2 {
font-size: 1.5em;
font-weight: 600;
- margin: 16px 0 10px;
+ margin: 24px 0 16px;
+ padding-bottom: 0.3em;
+ border-bottom: 1px solid $white-dark;
color: $gl-text-color;
}
h3 {
- margin: 16px 0 10px;
+ margin: 24px 0 16px;
font-size: 1.3em;
}
h4 {
- margin: 16px 0 10px;
+ margin: 24px 0 16px;
font-size: 1.2em;
}
h5 {
- margin: 16px 0 10px;
+ margin: 24px 0 16px;
font-size: 1em;
}
h6 {
- margin: 16px 0 10px;
+ margin: 24px 0 16px;
font-size: 0.95em;
}
blockquote {
color: $gl-grayish-blue;
font-size: inherit;
- padding: 8px 21px;
- margin: 12px 0;
+ padding: 8px 24px;
+ margin: 16px 0;
border-left: 3px solid $white-dark;
}
@@ -95,19 +108,20 @@
blockquote p {
color: $gl-grayish-blue !important;
+ margin: 0;
font-size: inherit;
line-height: 1.5;
}
p {
color: $gl-text-color;
- margin: 6px 0 0;
+ margin: 0 0 16px;
}
table {
@extend .table;
@extend .table-bordered;
- margin: 12px 0;
+ margin: 16px 0;
color: $gl-text-color;
th {
@@ -120,7 +134,7 @@
}
pre {
- margin: 12px 0;
+ margin-bottom: 16px;
font-size: 13px;
line-height: 1.6em;
overflow-x: auto;
@@ -134,7 +148,7 @@
ul,
ol {
padding: 0;
- margin: 3px 0 !important;
+ margin: 0 0 16px !important;
}
ul:dir(rtl),
@@ -338,3 +352,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 20ef9a774e4..49741c963df 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -111,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
@@ -458,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;
@@ -556,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 144adbcdaef..724b4080ee0 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -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;
+ }
}
}
@@ -189,6 +200,7 @@
.header-content {
flex: 1;
+ line-height: 1.8;
a {
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index ad3dbc7ac48..403724cd68a 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -93,11 +93,6 @@
top: $gl-padding-top;
}
- .bordered-box {
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
- }
-
.content-list {
li {
padding: 18px $gl-padding $gl-padding;
@@ -139,42 +134,9 @@
}
}
- .landing {
- margin-bottom: $gl-padding;
- overflow: hidden;
-
- .dismiss-icon {
- position: absolute;
- right: $cycle-analytics-box-padding;
- cursor: pointer;
- color: $cycle-analytics-dismiss-icon-color;
- }
-
- .svg-container {
- text-align: center;
-
- svg {
- width: 136px;
- height: 136px;
- }
- }
-
- .inner-content {
- @media (max-width: $screen-xs-max) {
- padding: 0 28px;
- text-align: center;
- }
-
- h4 {
- color: $gl-text-color;
- font-size: 17px;
- }
-
- p {
- color: $cycle-analytics-box-text-color;
- margin-bottom: $gl-padding;
- }
- }
+ .landing svg {
+ width: 136px;
+ height: 136px;
}
.fa-spinner {
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 46fd19c93f9..f3de05aa5f6 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -29,11 +29,5 @@
.description {
margin-top: 6px;
-
- p {
- &:last-child {
- margin-bottom: 0;
- }
- }
}
}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 73a5889867a..72d73b89a2a 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -88,3 +88,26 @@
color: $gl-text-color-secondary;
margin-top: 10px;
}
+
+.explore-groups.landing {
+ margin-top: 10px;
+
+ .inner-content {
+ padding: 0;
+
+ p {
+ margin: 7px 0 0;
+ max-width: 480px;
+ padding: 0 $gl-padding;
+
+ @media (max-width: $screen-sm-min) {
+ margin: 0 auto;
+ }
+ }
+ }
+
+ svg {
+ width: 62px;
+ height: 50px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 0bca3e93e4c..97fab513b01 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -52,7 +52,7 @@
.title {
padding: 0;
- margin: 0;
+ margin-bottom: 16px;
border-bottom: none;
}
@@ -210,10 +210,6 @@
}
}
- .bold {
- font-weight: 600;
- }
-
.light {
font-weight: normal;
}
@@ -361,6 +357,8 @@
}
.detail-page-description {
+ padding: 16px 0 0;
+
small {
color: $gray-darkest;
}
@@ -368,6 +366,8 @@
.edited-text {
color: $gray-darkest;
+ display: block;
+ margin: 0 0 16px;
.author_link {
color: $gray-darkest;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index b2f45625a2a..2aa52986e0a 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -101,11 +101,16 @@ ul.related-merge-requests > li {
}
}
-.merge-request-ci-status {
+.merge-request-ci-status,
+.related-merge-requests {
+ .ci-status-link {
+ display: block;
+ margin-top: 3px;
+ margin-right: 5px;
+ }
+
svg {
- margin-right: 4px;
- position: relative;
- top: 1px;
+ display: block;
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index b637994adf8..62f654ed343 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -28,7 +28,7 @@
.note-edit-form {
.note-form-actions {
position: relative;
- margin-top: $gl-padding;
+ margin: $gl-padding 0;
}
.note-preview-holder {
@@ -387,6 +387,7 @@
@media (max-width: $screen-xs-max) {
display: flex;
width: 100%;
+ margin-bottom: 10px;
.comment-btn {
flex-grow: 1;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 2ea2ff8362b..7cf74502a3a 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -97,18 +97,21 @@ ul.notes {
padding-left: 1.3em;
}
}
+
+ table {
+ @include markdown-table;
+ }
}
}
.note-awards {
.js-awards-block {
- padding: 2px;
- margin-top: 10px;
+ margin-bottom: 16px;
}
}
.note-header {
- padding-bottom: 3px;
+ padding-bottom: 8px;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
@@ -151,6 +154,10 @@ ul.notes {
margin-left: 65px;
}
+ .note-header {
+ padding-bottom: 0;
+ }
+
&.timeline-entry::after {
clear: none;
}
@@ -386,6 +393,10 @@ ul.notes {
.note-headline-meta {
display: inline-block;
white-space: nowrap;
+
+ .system-note-message {
+ white-space: normal;
+ }
}
/**
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 a9e2a034790..c119f0c9b22 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -596,6 +596,10 @@ pre.light-well {
.avatar-container {
align-self: flex-start;
+
+ > a {
+ width: 100%;
+ }
}
.project-details {
@@ -610,6 +614,7 @@ pre.light-well {
.controls {
margin-left: auto;
+ text-align: right;
}
.ci-status-link {
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/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index f3916622b6f..03c75ce61f5 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -160,7 +160,6 @@
.tree-controls {
float: right;
- margin-top: 11px;
position: relative;
z-index: 2;
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index 9bc47bbe173..04ff2d52b91 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -159,3 +159,9 @@ ul.wiki-pages-list.content-list {
padding: 5px 0;
}
}
+
+.wiki {
+ table {
+ @include markdown-table;
+ }
+}
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..5885b3543bb 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -28,7 +28,7 @@ class Admin::GroupsController < Admin::ApplicationController
if @group.save
@group.add_owner(current_user)
- redirect_to [:admin, @group], notice: 'Group was successfully created.'
+ redirect_to [:admin, @group], notice: "Group '#{@group.name}' was successfully created."
else
render "new"
end
@@ -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/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index cbfc4581411..a119934febc 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -1,4 +1,6 @@
class Admin::HooksController < Admin::ApplicationController
+ before_action :hook, only: :edit
+
def index
@hooks = SystemHook.all
@hook = SystemHook.new
@@ -15,15 +17,25 @@ class Admin::HooksController < Admin::ApplicationController
end
end
+ def edit
+ end
+
+ def update
+ if hook.update_attributes(hook_params)
+ flash[:notice] = 'System hook was successfully updated.'
+ redirect_to admin_hooks_path
+ else
+ render 'edit'
+ end
+ end
+
def destroy
- @hook = SystemHook.find(params[:id])
- @hook.destroy
+ hook.destroy
redirect_to admin_hooks_path
end
def test
- @hook = SystemHook.find(params[:hook_id])
data = {
event_name: "project_create",
name: "Ruby",
@@ -32,11 +44,17 @@ class Admin::HooksController < Admin::ApplicationController
owner_name: "Someone",
owner_email: "example@gitlabhq.com"
}
- @hook.execute(data, 'system_hooks')
+ hook.execute(data, 'system_hooks')
redirect_back_or_default
end
+ private
+
+ def hook
+ @hook ||= SystemHook.find(params[:id])
+ end
+
def hook_params
params.require(:hook).permit(
:enable_ssl_verification,
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index e77094fe2a8..e48f0963ef4 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -118,6 +118,10 @@ class ApplicationController < ActionController::Base
end
end
+ def respond_422
+ head :unprocessable_entity
+ end
+
def no_cache_headers
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
response.headers["Pragma"] = "no-cache"
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/markdown_preview.rb b/app/controllers/concerns/markdown_preview.rb
new file mode 100644
index 00000000000..40eff267348
--- /dev/null
+++ b/app/controllers/concerns/markdown_preview.rb
@@ -0,0 +1,19 @@
+module MarkdownPreview
+ private
+
+ def render_markdown_preview(text, markdown_context = {})
+ render json: {
+ body: view_context.markdown(text, markdown_context),
+ references: {
+ users: preview_referenced_users(text)
+ }
+ }
+ end
+
+ def preview_referenced_users(text)
+ extractor = Gitlab::ReferenceExtractor.new(@project, current_user)
+ extractor.analyze(text, author: current_user)
+
+ extractor.users.map(&:username)
+ 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/concerns/renders_blob.rb b/app/controllers/concerns/renders_blob.rb
new file mode 100644
index 00000000000..9faf68e6d97
--- /dev/null
+++ b/app/controllers/concerns/renders_blob.rb
@@ -0,0 +1,21 @@
+module RendersBlob
+ extend ActiveSupport::Concern
+
+ def render_blob_json(blob)
+ viewer =
+ if params[:viewer] == 'rich'
+ blob.rich_viewer
+ else
+ blob.simple_viewer
+ end
+ return render_404 unless viewer
+
+ render json: {
+ html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_asynchronously: false)
+ }
+ end
+
+ def override_max_blob_size(blob)
+ blob.override_max_size! if params[:override_max_size] == 'true'
+ end
+end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index a8c0937569c..be2e6c7f193 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -38,6 +38,7 @@ module ServiceParams
:new_issue_url,
:notify,
:notify_only_broken_pipelines,
+ :notify_only_default_branch,
:password,
:priority,
:project_key,
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..9489bbddfc4 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -2,6 +2,7 @@
class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
include CreatesCommit
+ include RendersBlob
include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path
@@ -25,17 +26,29 @@ 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
def show
- environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
- @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+ override_max_blob_size(@blob)
+
+ respond_to do |format|
+ format.html do
+ environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+
+ render 'show'
+ end
+
+ format.json do
+ render_blob_json(@blob)
+ end
+ end
end
def edit
@@ -69,10 +82,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
@@ -96,7 +109,7 @@ class Projects::BlobController < Projects::ApplicationController
private
def blob
- @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
+ @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path), @project)
if @blob
@blob
@@ -127,16 +140,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/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 04e8cdf6256..e24fc45d166 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,6 +1,6 @@
class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
- before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play]
+ before_action :authorize_read_build!, only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace]
layout 'project'
@@ -60,20 +60,22 @@ class Projects::BuildsController < Projects::ApplicationController
end
def retry
- return render_404 unless @build.retryable?
+ return respond_422 unless @build.retryable?
build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build)
end
def play
- return render_404 unless @build.playable?
+ return respond_422 unless @build.playable?
build = @build.play(current_user)
redirect_to build_path(build)
end
def cancel
+ return respond_422 unless @build.cancelable?
+
@build.cancel
redirect_to build_path(@build)
end
@@ -85,9 +87,12 @@ class Projects::BuildsController < Projects::ApplicationController
end
def erase
- @build.erase(erased_by: current_user)
- redirect_to namespace_project_build_path(project.namespace, project, @build),
+ if @build.erase(erased_by: current_user)
+ redirect_to namespace_project_build_path(project.namespace, project, @build),
notice: "Build has been successfully erased!"
+ else
+ respond_422
+ end
end
def raw
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/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 1e41f980f31..86d13a0d222 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -1,6 +1,7 @@
class Projects::HooksController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
+ before_action :hook, only: :edit
respond_to :html
@@ -17,6 +18,18 @@ class Projects::HooksController < Projects::ApplicationController
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
+ def edit
+ end
+
+ def update
+ if hook.update_attributes(hook_params)
+ flash[:notice] = 'Hook was successfully updated.'
+ redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
+ else
+ render 'edit'
+ end
+ end
+
def test
if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user)
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 408c0c60cb0..d0dd524c484 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -23,6 +23,7 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to do |format|
format.html do
+ @project_namespace = @project.namespace.becomes(Namespace)
@milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page])
end
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/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index c55b37ae0dd..a0b08ad130f 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
return if cached_blob?
- if @blob.lfs_pointer? && project.lfs_enabled?
+ if @blob.valid_lfs_pointer?
send_lfs_object
else
send_git_blob @repository, @blob
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
index fb2a4837735..1ff08cce8cb 100644
--- a/app/controllers/projects/settings/integrations_controller.rb
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -5,7 +5,7 @@ module Projects
before_action :authorize_admin_project!
layout "project_settings"
-
+
def show
@hooks = @project.hooks
@hook = ProjectHook.new
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 5c9e0d4d1a1..66f913f8f9d 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -3,6 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji
include SpammableActions
include SnippetsActions
+ include RendersBlob
before_action :module_enabled
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
@@ -55,11 +56,23 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def show
- @note = @project.notes.new(noteable: @snippet)
- @noteable = @snippet
-
- @discussions = @snippet.discussions
- @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
+ blob = @snippet.blob
+ override_max_blob_size(blob)
+
+ respond_to do |format|
+ format.html do
+ @note = @project.notes.new(noteable: @snippet)
+ @noteable = @snippet
+
+ @discussions = @snippet.discussions
+ @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
+ render 'show'
+ end
+
+ format.json do
+ render_blob_json(blob)
+ end
+ end
end
def destroy
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/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index c5e24b9e365..96125684da0 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -1,4 +1,6 @@
class Projects::WikisController < Projects::ApplicationController
+ include MarkdownPreview
+
before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy
@@ -91,21 +93,13 @@ class Projects::WikisController < Projects::ApplicationController
)
end
- def preview_markdown
- text = params[:text]
-
- ext = Gitlab::ReferenceExtractor.new(@project, current_user)
- ext.analyze(text, author: current_user)
-
- render json: {
- body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
- references: {
- users: ext.users.map(&:username)
- }
- }
+ def git_access
end
- def git_access
+ def preview_markdown
+ context = { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
+
+ render_markdown_preview(params[:text], context)
end
private
@@ -115,7 +109,6 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
-
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 6807c37f972..9f6ee4826e6 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,6 +1,7 @@
class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
+ include MarkdownPreview
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create]
@@ -216,20 +217,6 @@ class ProjectsController < Projects::ApplicationController
}
end
- def preview_markdown
- text = params[:text]
-
- ext = Gitlab::ReferenceExtractor.new(@project, current_user)
- ext.analyze(text, author: current_user)
-
- render json: {
- body: view_context.markdown(text),
- references: {
- users: ext.users.map(&:username)
- }
- }
- end
-
def refs
branches = BranchesFinder.new(@repository, params).execute.map(&:name)
@@ -252,6 +239,10 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json
end
+ def preview_markdown
+ render_markdown_preview(params[:text])
+ end
+
private
# Render project landing depending of which features are available
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/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index f3fd3da8b20..906833505d1 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -2,6 +2,8 @@ class SnippetsController < ApplicationController
include ToggleAwardEmoji
include SpammableActions
include SnippetsActions
+ include MarkdownPreview
+ include RendersBlob
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
@@ -59,6 +61,18 @@ class SnippetsController < ApplicationController
end
def show
+ blob = @snippet.blob
+ override_max_blob_size(blob)
+
+ respond_to do |format|
+ format.html do
+ render 'show'
+ end
+
+ format.json do
+ render_blob_json(blob)
+ end
+ end
end
def destroy
@@ -77,6 +91,10 @@ class SnippetsController < ApplicationController
)
end
+ def preview_markdown
+ render_markdown_preview(params[:text], skip_project_check: true)
+ end
+
protected
def snippet
diff --git a/app/controllers/unicorn_test_controller.rb b/app/controllers/unicorn_test_controller.rb
new file mode 100644
index 00000000000..b7a1a046be0
--- /dev/null
+++ b/app/controllers/unicorn_test_controller.rb
@@ -0,0 +1,12 @@
+if Rails.env.test?
+ class UnicornTestController < ActionController::Base
+ def pid
+ render plain: Process.pid.to_s
+ end
+
+ def kill
+ Process.kill(params[:signal], Process.pid)
+ render plain: 'Bye!'
+ end
+ end
+end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 42f0ebd774c..2fc34f186ad 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -6,7 +6,7 @@
# current_user - which user use
# params:
# scope: 'created-by-me' or 'assigned-to-me' or 'all'
-# state: 'open' or 'closed' or 'all'
+# state: 'open', 'closed', 'merged', or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index e5b811f3300..fff57472a4f 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -196,38 +196,6 @@ module ApplicationHelper
end
end
- def render_markup(file_name, file_content)
- if gitlab_markdown?(file_name)
- Hamlit::RailsHelpers.preserve(markdown(file_content))
- elsif asciidoc?(file_name)
- asciidoc(file_content)
- elsif plain?(file_name)
- content_tag :pre, class: 'plain-readme' do
- file_content
- end
- else
- other_markup(file_name, file_content)
- end
- rescue RuntimeError
- simple_format(file_content)
- end
-
- def plain?(filename)
- Gitlab::MarkupHelper.plain?(filename)
- end
-
- def markup?(filename)
- Gitlab::MarkupHelper.markup?(filename)
- end
-
- def gitlab_markdown?(filename)
- Gitlab::MarkupHelper.gitlab_markdown?(filename)
- end
-
- def asciidoc?(filename)
- Gitlab::MarkupHelper.asciidoc?(filename)
- end
-
def promo_host
'about.gitlab.com'
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 4b3ab03a69c..377b080b3c6 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
@@ -37,7 +28,16 @@ module BlobHelper
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(project, ref, path, options),
+ 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' }
- 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' }
+ 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.valid_lfs_pointer?
+ 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: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
+ 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
@@ -91,7 +95,7 @@ module BlobHelper
end
def can_modify_blob?(blob, project = @project, ref = @ref)
- !blob.lfs_pointer? && can_edit_tree?(project, ref)
+ !blob.valid_lfs_pointer? && can_edit_tree?(project, ref)
end
def leave_edit_message
@@ -114,28 +118,23 @@ module BlobHelper
icon("#{file_type_icon_class('file', mode, name)} fw")
end
- def blob_text_viewable?(blob)
- 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
- else
- blob.size
+ def blob_raw_url
+ if @snippet
+ if @snippet.project_id
+ raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
+ else
+ raw_snippet_path(@snippet)
+ end
+ elsif @blob
+ namespace_project_raw_path(@project.namespace, @project, @id)
end
end
# SVGs can contain malicious JavaScript; only include whitelisted
# elements and attributes. Note that this whitelist is by no means complete
# and may omit some elements.
- def sanitize_svg(blob)
- blob.data = Gitlab::Sanitizers::SVG.clean(blob.data)
- blob
+ def sanitize_svg_data(data)
+ Gitlab::Sanitizers::SVG.clean(data)
end
# If we blindly set the 'real' content type when serving a Git blob we
@@ -217,13 +216,52 @@ module BlobHelper
clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
end
- def copy_blob_content_button(blob)
- return if markup?(blob.name)
+ def copy_blob_source_button(blob)
+ return unless blob.rendered_as_text?(ignore_errors: false)
+
+ clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard")
+ end
+
+ def open_raw_blob_button(blob)
+ if blob.raw_binary?
+ icon = icon('download')
+ title = 'Download'
+ else
+ icon = icon('file-code-o')
+ title = 'Open raw'
+ end
- clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard")
+ link_to icon, blob_raw_url, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end
- def open_raw_file_button(path)
- link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
+ def blob_render_error_reason(viewer)
+ case viewer.render_error
+ when :too_large
+ max_size =
+ if viewer.absolutely_too_large?
+ viewer.absolute_max_size
+ elsif viewer.too_large?
+ viewer.max_size
+ end
+ "it is larger than #{number_to_human_size(max_size)}"
+ when :server_side_but_stored_in_lfs
+ "it is stored in LFS"
+ end
+ end
+
+ def blob_render_error_options(viewer)
+ options = []
+
+ if viewer.render_error == :too_large && viewer.can_override_max_size?
+ options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil)))
+ end
+
+ if viewer.rich? && viewer.blob.rendered_as_text?
+ options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' })
+ end
+
+ options << link_to('download it', blob_raw_url, target: '_blank', rel: 'noopener noreferrer')
+
+ options
end
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 2de9e0de310..32b1e7822af 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -1,10 +1,16 @@
+##
+# DEPRECATED
+#
+# These helpers are deprecated in favor of detailed CI/CD statuses.
+#
+# See 'detailed_status?` method and `Gitlab::Ci::Status` module.
+#
module CiStatusHelper
def ci_status_path(pipeline)
project = pipeline.project
namespace_project_pipeline_path(project.namespace, project, pipeline)
end
- # Is used by Commit and Merge Request Widget
def ci_label_for_status(status)
if detailed_status?(status)
return status.label
@@ -22,6 +28,23 @@ module CiStatusHelper
end
end
+ def ci_text_for_status(status)
+ if detailed_status?(status)
+ return status.text
+ end
+
+ case status
+ when 'success'
+ 'passed'
+ when 'success_with_warnings'
+ 'passed'
+ when 'manual'
+ 'blocked'
+ else
+ status
+ end
+ end
+
def ci_status_for_statuseable(subject)
status = subject.try(:status) || 'not found'
status.humanize
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 5f5c76d3722..960111ca045 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -10,11 +10,12 @@ module EventsHelper
'deleted' => 'icon_trash_o'
}.freeze
- def link_to_author(event)
+ def link_to_author(event, self_added: false)
author = event.author
if author
- link_to author.name, user_path(author.username), title: author.name
+ name = self_added ? 'You' : author.name
+ link_to name, user_path(author.username), title: name
else
event.author_name
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index ab3ef454e1c..55fa81e95ef 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -7,6 +7,11 @@ module IconsHelper
# font-awesome-rails gem, but should we ever use a different icon pack in the
# future we won't have to change hundreds of method calls.
def icon(names, options = {})
+ if (options.keys & %w[aria-hidden aria-label]).empty?
+ # Add `aria-hidden` if there are no aria's set
+ options['aria-hidden'] = true
+ end
+
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
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/gitlab_markdown_helper.rb b/app/helpers/markup_helper.rb
index cd442237086..b241a14740b 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -1,6 +1,22 @@
require 'nokogiri'
-module GitlabMarkdownHelper
+module MarkupHelper
+ def plain?(filename)
+ Gitlab::MarkupHelper.plain?(filename)
+ end
+
+ def markup?(filename)
+ Gitlab::MarkupHelper.markup?(filename)
+ end
+
+ def gitlab_markdown?(filename)
+ Gitlab::MarkupHelper.gitlab_markdown?(filename)
+ end
+
+ def asciidoc?(filename)
+ Gitlab::MarkupHelper.asciidoc?(filename)
+ end
+
# Use this in places where you would normally use link_to(gfm(...), ...).
#
# It solves a problem occurring with nested links (i.e.
@@ -11,7 +27,7 @@ module GitlabMarkdownHelper
# explicitly produce the correct linking behavior (i.e.
# "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
def link_to_gfm(body, url, html_options = {})
- return "" if body.blank?
+ return '' if body.blank?
context = {
project: @project,
@@ -43,71 +59,73 @@ module GitlabMarkdownHelper
fragment.to_html.html_safe
end
+ # Return the first line of +text+, up to +max_chars+, after parsing the line
+ # as Markdown. HTML tags in the parsed output are not counted toward the
+ # +max_chars+ limit. If the length limit falls within a tag's contents, then
+ # the tag contents are truncated without removing the closing tag.
+ def first_line_in_markdown(text, max_chars = nil, options = {})
+ md = markdown(text, options).strip
+
+ truncate_visible(md, max_chars || md.length) if md.present?
+ end
+
def markdown(text, context = {})
- return "" unless text.present?
+ return '' unless text.present?
context[:project] ||= @project
-
- html = Banzai.render(text, context)
- banzai_postprocess(html, context)
+ html = markdown_unsafe(text, context)
+ prepare_for_rendering(html, context)
end
def markdown_field(object, field)
object = object.for_display if object.respond_to?(:for_display)
- return "" unless object.present?
+ return '' unless object.present?
html = Banzai.render_field(object, field)
- banzai_postprocess(html, object.banzai_render_context(field))
+ prepare_for_rendering(html, object.banzai_render_context(field))
end
- def asciidoc(text)
- Gitlab::Asciidoc.render(
- text,
- project: @project,
- current_user: (current_user if defined?(current_user)),
-
- # RelativeLinkFilter
- project_wiki: @project_wiki,
- requested_path: @path,
- ref: @ref,
- commit: @commit
- )
+ def markup(file_name, text, context = {})
+ context[:project] ||= @project
+ html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
+ prepare_for_rendering(html, context)
end
- def other_markup(file_name, text)
- Gitlab::OtherMarkup.render(
- file_name,
- text,
- project: @project,
- current_user: (current_user if defined?(current_user)),
+ def render_wiki_content(wiki_page)
+ text = wiki_page.content
+ return '' unless text.present?
+
+ context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug }
+
+ html =
+ case wiki_page.format
+ when :markdown
+ markdown_unsafe(text, context)
+ when :asciidoc
+ asciidoc_unsafe(text)
+ else
+ wiki_page.formatted_content.html_safe
+ end
- # RelativeLinkFilter
- project_wiki: @project_wiki,
- requested_path: @path,
- ref: @ref,
- commit: @commit
- )
+ prepare_for_rendering(html, context)
end
- # Return the first line of +text+, up to +max_chars+, after parsing the line
- # as Markdown. HTML tags in the parsed output are not counted toward the
- # +max_chars+ limit. If the length limit falls within a tag's contents, then
- # the tag contents are truncated without removing the closing tag.
- def first_line_in_markdown(text, max_chars = nil, options = {})
- md = markdown(text, options).strip
+ def markup_unsafe(file_name, text, context = {})
+ return '' unless text.present?
- truncate_visible(md, max_chars || md.length) if md.present?
- end
-
- def render_wiki_content(wiki_page)
- case wiki_page.format
- when :markdown
- markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug)
- when :asciidoc
- asciidoc(wiki_page.content)
+ if gitlab_markdown?(file_name)
+ markdown_unsafe(text, context)
+ elsif asciidoc?(file_name)
+ asciidoc_unsafe(text)
+ elsif plain?(file_name)
+ content_tag :pre, class: 'plain-readme' do
+ text
+ end
else
- wiki_page.formatted_content.html_safe
+ other_markup_unsafe(file_name, text)
end
+ rescue RuntimeError
+ simple_format(text)
end
# Returns the text necessary to reference `entity` across projects
@@ -183,10 +201,10 @@ module GitlabMarkdownHelper
end
def markdown_toolbar_button(options = {})
- data = options[:data].merge({ container: "body" })
+ data = options[:data].merge({ container: 'body' })
content_tag :button,
- type: "button",
- class: "toolbar-btn js-md has-tooltip hidden-xs",
+ type: 'button',
+ class: 'toolbar-btn js-md has-tooltip hidden-xs',
tabindex: -1,
data: data,
title: options[:title],
@@ -195,17 +213,35 @@ module GitlabMarkdownHelper
end
end
- # Calls Banzai.post_process with some common context options
- def banzai_postprocess(html, context)
+ def markdown_unsafe(text, context = {})
+ Banzai.render(text, context)
+ end
+
+ def asciidoc_unsafe(text)
+ Gitlab::Asciidoc.render(text)
+ end
+
+ def other_markup_unsafe(file_name, text)
+ Gitlab::OtherMarkup.render(file_name, text)
+ end
+
+ def prepare_for_rendering(html, context = {})
+ return '' unless html.present?
+
context.merge!(
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
- requested_path: @path,
+ commit: @commit,
project_wiki: @project_wiki,
- ref: @ref
+ ref: @ref,
+ requested_path: @path
)
- Banzai.post_process(html, context)
+ html = Banzai.post_process(html, context)
+
+ Hamlit::RailsHelpers.preserve(html)
end
+
+ extend self
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 38be073c8dc..2614cdfe90e 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -1,6 +1,6 @@
module MergeRequestsHelper
def new_mr_path_from_push_event(event)
- target_project = event.project.forked_from_project || event.project
+ target_project = event.project.default_merge_request_target
new_namespace_project_merge_request_path(
event.project.namespace,
event.project,
@@ -56,11 +56,12 @@ module MergeRequestsHelper
end
def issues_sentence(issues)
- # Sorting based on the `#123` or `group/project#123` reference will sort
- # local issues first.
- issues.map do |issue|
+ # Issuable sorter will sort local issues, then issues from the same
+ # namespace, then all other issues.
+ issues = Gitlab::IssuableSorter.sort(@project, issues).map do |issue|
issue.to_reference(@project)
- end.sort.to_sentence
+ end
+ issues.to_sentence
end
def mr_closes_issues
@@ -126,6 +127,10 @@ module MergeRequestsHelper
end
end
+ def target_projects(project)
+ [project, project.default_merge_request_target].uniq
+ end
+
def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 43669b6f356..8c26348a975 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -160,12 +160,17 @@ module ProjectsHelper
end
def project_list_cache_key(project)
- key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.3']
+ key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.4']
key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
key
end
+ def load_pipeline_status(projects)
+ Gitlab::Cache::Ci::ProjectPipelineStatus.
+ load_in_batch_for_projects(projects)
+ end
+
private
def repo_children_classes(field)
@@ -272,14 +277,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
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 715e5893a2c..3707bb5ba36 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -13,8 +13,8 @@ module ServicesHelper
"Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged"
- when "build", "build_events"
- "Event will be triggered when a build status changes"
+ when "pipeline", "pipeline_events"
+ "Event will be triggered when a pipeline status changes"
when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events"
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/todos_helper.rb b/app/helpers/todos_helper.rb
index 4f5adf623f2..f19e2f9db9c 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -13,13 +13,13 @@ module TodosHelper
def todo_action_name(todo)
case todo.action
- when Todo::ASSIGNED then 'assigned you'
- when Todo::MENTIONED then 'mentioned you on'
+ when Todo::ASSIGNED then todo.self_added? ? 'assigned' : 'assigned you'
+ when Todo::MENTIONED then "mentioned #{todo_action_subject(todo)} on"
when Todo::BUILD_FAILED then 'The build failed for'
when Todo::MARKED then 'added a todo for'
- when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
+ when Todo::APPROVAL_REQUIRED then "set #{todo_action_subject(todo)} as an approver for"
when Todo::UNMERGEABLE then 'Could not merge'
- when Todo::DIRECTLY_ADDRESSED then 'directly addressed you on'
+ when Todo::DIRECTLY_ADDRESSED then "directly addressed #{todo_action_subject(todo)} on"
end
end
@@ -148,6 +148,10 @@ module TodosHelper
private
+ def todo_action_subject(todo)
+ todo.self_added? ? 'yourself' : 'you'
+ end
+
def show_todo_state?(todo)
(todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 4a76c679bad..f7b5a5f4dfc 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -12,10 +12,6 @@ module TreeHelper
tree.html_safe
end
- def render_readme(readme)
- render_markup(readme.name, readme.data)
- end
-
# Return an image icon depending on the file type and mode
#
# type - String type of the tree item; either 'folder' or 'file'
@@ -35,7 +31,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/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index a9b6b33eb5c..d2980db218a 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -1,6 +1,6 @@
class BaseMailer < ActionMailer::Base
helper ApplicationHelper
- helper GitlabMarkdownHelper
+ helper MarkupHelper
attr_accessor :current_user
helper_method :current_user, :can?
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 2961e16f5e0..cf042717c95 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -28,6 +28,8 @@ class ApplicationSetting < ActiveRecord::Base
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
+ validates :uuid, presence: true
+
validates :session_expire_delay,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
@@ -159,6 +161,7 @@ class ApplicationSetting < ActiveRecord::Base
end
end
+ before_validation :ensure_uuid!
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
@@ -238,7 +241,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
@@ -343,6 +347,12 @@ class ApplicationSetting < ActiveRecord::Base
private
+ def ensure_uuid!
+ return if uuid?
+
+ self.uuid = SecureRandom.uuid
+ end
+
def check_repository_storages
invalid = repository_storages - Gitlab.config.repositories.storages.keys
errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 55872acef51..1cdb8811cff 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -3,8 +3,42 @@ class Blob < SimpleDelegator
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
- # The maximum size of an SVG that can be displayed.
- MAXIMUM_SVG_SIZE = 2.megabytes
+ MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte
+
+ # Finding a viewer for a blob happens based only on extension and whether the
+ # blob is binary or text, which means 1 blob should only be matched by 1 viewer,
+ # and the order of these viewers doesn't really matter.
+ #
+ # However, when the blob is an LFS pointer, we cannot know for sure whether the
+ # file being pointed to is binary or text. In this case, we match only on
+ # extension, preferring binary viewers over text ones if both exist, since the
+ # large files referred to in "Large File Storage" are much more likely to be
+ # binary than text.
+ #
+ # `.stl` files, for example, exist in both binary and text forms, and are
+ # handled by different viewers (`BinarySTL` and `TextSTL`) depending on blob
+ # type. LFS pointers to `.stl` files are assumed to always be the binary kind,
+ # and use the `BinarySTL` viewer.
+ RICH_VIEWERS = [
+ BlobViewer::Markup,
+ BlobViewer::Notebook,
+ BlobViewer::SVG,
+
+ BlobViewer::Image,
+ BlobViewer::Sketch,
+
+ BlobViewer::Video,
+
+ BlobViewer::PDF,
+
+ BlobViewer::BinarySTL,
+ BlobViewer::TextSTL,
+ ].freeze
+
+ BINARY_VIEWERS = RICH_VIEWERS.select(&:binary?).freeze
+ TEXT_VIEWERS = RICH_VIEWERS.select(&:text?).freeze
+
+ attr_reader :project
# Wrap a Gitlab::Git::Blob object, or return nil when given nil
#
@@ -16,10 +50,16 @@ class Blob < SimpleDelegator
#
# blob = Blob.decorate(nil)
# puts "truthy" if blob # No output
- def self.decorate(blob)
+ def self.decorate(blob, project = nil)
return if blob.nil?
- new(blob)
+ new(blob, project)
+ end
+
+ def initialize(blob, project = nil)
+ @project = project
+
+ super(blob)
end
# Returns the data of the blob.
@@ -35,82 +75,107 @@ class Blob < SimpleDelegator
end
def no_highlighting?
- size && size > 1.megabyte
+ size && size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
end
- def only_display_raw?
+ def too_large?
size && truncated?
end
+ # Returns the size of the file that this blob represents. If this blob is an
+ # LFS pointer, this is the size of the file stored in LFS. Otherwise, this is
+ # the size of the blob itself.
+ def raw_size
+ if valid_lfs_pointer?
+ lfs_size
+ else
+ size
+ end
+ end
+
+ # Returns whether the file that this blob represents is binary. If this blob is
+ # an LFS pointer, we assume the file stored in LFS is binary, unless a
+ # text-based rich blob viewer matched on the file's extension. Otherwise, this
+ # depends on the type of the blob itself.
+ def raw_binary?
+ if valid_lfs_pointer?
+ if rich_viewer
+ rich_viewer.binary?
+ else
+ true
+ end
+ else
+ binary?
+ end
+ end
+
def extension
- extname.downcase.delete('.')
+ @extension ||= extname.downcase.delete('.')
end
- def svg?
- text? && language && language.name == 'SVG'
+ def video?
+ UploaderHelper::VIDEO_EXT.include?(extension)
end
- def pdf?
- extension == 'pdf'
+ def readable_text?
+ text? && !valid_lfs_pointer? && !too_large?
end
- def ipython_notebook?
- text? && language&.name == 'Jupyter Notebook'
+ def valid_lfs_pointer?
+ lfs_pointer? && project&.lfs_enabled?
end
- def sketch?
- binary? && extension == 'sketch'
+ def invalid_lfs_pointer?
+ lfs_pointer? && !project&.lfs_enabled?
end
- def stl?
- extension == 'stl'
+ def simple_viewer
+ @simple_viewer ||= simple_viewer_class.new(self)
end
- def markup?
- text? && Gitlab::MarkupHelper.markup?(name)
+ def rich_viewer
+ return @rich_viewer if defined?(@rich_viewer)
+
+ @rich_viewer = rich_viewer_class&.new(self)
end
- def size_within_svg_limits?
- size <= MAXIMUM_SVG_SIZE
+ def rendered_as_text?(ignore_errors: true)
+ simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
end
- def video?
- UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
+ def show_viewer_switcher?
+ rendered_as_text? && rich_viewer
end
- def to_partial_path(project)
- if lfs_pointer?
- if project.lfs_enabled?
- 'download'
- else
- 'text'
- end
- elsif image?
- 'image'
- elsif svg?
- 'svg'
- elsif pdf?
- 'pdf'
- elsif ipython_notebook?
- 'notebook'
- elsif sketch?
- 'sketch'
- elsif stl?
- 'stl'
- elsif markup?
- if only_display_raw?
- 'too_large'
- else
- 'markup'
- end
- elsif text?
- if only_display_raw?
- 'too_large'
- else
- 'text'
- end
- else
- 'download'
+ def override_max_size!
+ simple_viewer&.override_max_size = true
+ rich_viewer&.override_max_size = true
+ end
+
+ private
+
+ def simple_viewer_class
+ if empty?
+ BlobViewer::Empty
+ elsif raw_binary?
+ BlobViewer::Download
+ else # text
+ BlobViewer::Text
end
end
+
+ def rich_viewer_class
+ return if invalid_lfs_pointer? || empty?
+
+ classes =
+ if valid_lfs_pointer?
+ BINARY_VIEWERS + TEXT_VIEWERS
+ elsif binary?
+ BINARY_VIEWERS
+ else # text
+ TEXT_VIEWERS
+ end
+
+ classes.find { |viewer_class| viewer_class.can_render?(self) }
+ end
end
diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb
new file mode 100644
index 00000000000..f944b00c9d3
--- /dev/null
+++ b/app/models/blob_viewer/base.rb
@@ -0,0 +1,96 @@
+module BlobViewer
+ class Base
+ class_attribute :partial_name, :type, :extensions, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size
+
+ delegate :partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class
+
+ attr_reader :blob
+ attr_accessor :override_max_size
+
+ def initialize(blob)
+ @blob = blob
+ end
+
+ def self.partial_path
+ "projects/blob/viewers/#{partial_name}"
+ end
+
+ def self.rich?
+ type == :rich
+ end
+
+ def self.simple?
+ type == :simple
+ end
+
+ def self.client_side?
+ client_side
+ end
+
+ def self.server_side?
+ !client_side?
+ end
+
+ def self.binary?
+ binary
+ end
+
+ def self.text?
+ !binary?
+ end
+
+ def self.can_render?(blob)
+ !extensions || extensions.include?(blob.extension)
+ end
+
+ def too_large?
+ blob.raw_size > max_size
+ end
+
+ def absolutely_too_large?
+ blob.raw_size > absolute_max_size
+ end
+
+ def can_override_max_size?
+ too_large? && !absolutely_too_large?
+ end
+
+ # This method is used on the server side to check whether we can attempt to
+ # render the blob at all. Human-readable error messages are found in the
+ # `BlobHelper#blob_render_error_reason` helper.
+ #
+ # This method does not and should not load the entire blob contents into
+ # memory, and should not be overridden to do so in order to validate the
+ # format of the blob.
+ #
+ # Prefer to implement a client-side viewer, where the JS component loads the
+ # binary from `blob_raw_url` and does its own format validation and error
+ # rendering, especially for potentially large binary formats.
+ def render_error
+ return @render_error if defined?(@render_error)
+
+ @render_error =
+ if server_side_but_stored_in_lfs?
+ # Files stored in LFS can only be rendered using a client-side viewer,
+ # since we do not want to read large amounts of data into memory on the
+ # server side. Client-side viewers use JS and can fetch the file from
+ # `blob_raw_url` using AJAX.
+ :server_side_but_stored_in_lfs
+ elsif override_max_size ? absolutely_too_large? : too_large?
+ :too_large
+ end
+ end
+
+ def prepare!
+ if server_side? && blob.project
+ blob.load_all_data!(blob.project.repository)
+ end
+ end
+
+ private
+
+ def server_side_but_stored_in_lfs?
+ server_side? && blob.valid_lfs_pointer?
+ end
+ end
+end
diff --git a/app/models/blob_viewer/binary_stl.rb b/app/models/blob_viewer/binary_stl.rb
new file mode 100644
index 00000000000..80393471ef2
--- /dev/null
+++ b/app/models/blob_viewer/binary_stl.rb
@@ -0,0 +1,10 @@
+module BlobViewer
+ class BinarySTL < Base
+ include Rich
+ include ClientSide
+
+ self.partial_name = 'stl'
+ self.extensions = %w(stl)
+ self.binary = true
+ end
+end
diff --git a/app/models/blob_viewer/client_side.rb b/app/models/blob_viewer/client_side.rb
new file mode 100644
index 00000000000..42ec68f864b
--- /dev/null
+++ b/app/models/blob_viewer/client_side.rb
@@ -0,0 +1,11 @@
+module BlobViewer
+ module ClientSide
+ extend ActiveSupport::Concern
+
+ included do
+ self.client_side = true
+ self.max_size = 10.megabytes
+ self.absolute_max_size = 50.megabytes
+ end
+ end
+end
diff --git a/app/models/blob_viewer/download.rb b/app/models/blob_viewer/download.rb
new file mode 100644
index 00000000000..adc06587f69
--- /dev/null
+++ b/app/models/blob_viewer/download.rb
@@ -0,0 +1,17 @@
+module BlobViewer
+ class Download < Base
+ include Simple
+ # We treat the Download viewer as if it renders the content client-side,
+ # so that it doesn't attempt to load the entire blob contents and is
+ # rendered synchronously instead of loaded asynchronously.
+ include ClientSide
+
+ self.partial_name = 'download'
+ self.binary = true
+
+ # We can always render the Download viewer, even if the blob is in LFS or too large.
+ def render_error
+ nil
+ end
+ end
+end
diff --git a/app/models/blob_viewer/empty.rb b/app/models/blob_viewer/empty.rb
new file mode 100644
index 00000000000..d9d128eb273
--- /dev/null
+++ b/app/models/blob_viewer/empty.rb
@@ -0,0 +1,9 @@
+module BlobViewer
+ class Empty < Base
+ include Simple
+ include ServerSide
+
+ self.partial_name = 'empty'
+ self.binary = true
+ end
+end
diff --git a/app/models/blob_viewer/image.rb b/app/models/blob_viewer/image.rb
new file mode 100644
index 00000000000..c4eae5c79c2
--- /dev/null
+++ b/app/models/blob_viewer/image.rb
@@ -0,0 +1,12 @@
+module BlobViewer
+ class Image < Base
+ include Rich
+ include ClientSide
+
+ self.partial_name = 'image'
+ self.extensions = UploaderHelper::IMAGE_EXT
+ self.binary = true
+ self.switcher_icon = 'picture-o'
+ self.switcher_title = 'image'
+ end
+end
diff --git a/app/models/blob_viewer/markup.rb b/app/models/blob_viewer/markup.rb
new file mode 100644
index 00000000000..8fdbab30dd1
--- /dev/null
+++ b/app/models/blob_viewer/markup.rb
@@ -0,0 +1,10 @@
+module BlobViewer
+ class Markup < Base
+ include Rich
+ include ServerSide
+
+ self.partial_name = 'markup'
+ self.extensions = Gitlab::MarkupHelper::EXTENSIONS
+ self.binary = false
+ end
+end
diff --git a/app/models/blob_viewer/notebook.rb b/app/models/blob_viewer/notebook.rb
new file mode 100644
index 00000000000..8632b8a9885
--- /dev/null
+++ b/app/models/blob_viewer/notebook.rb
@@ -0,0 +1,12 @@
+module BlobViewer
+ class Notebook < Base
+ include Rich
+ include ClientSide
+
+ self.partial_name = 'notebook'
+ self.extensions = %w(ipynb)
+ self.binary = false
+ self.switcher_icon = 'file-text-o'
+ self.switcher_title = 'notebook'
+ end
+end
diff --git a/app/models/blob_viewer/pdf.rb b/app/models/blob_viewer/pdf.rb
new file mode 100644
index 00000000000..65805f5f388
--- /dev/null
+++ b/app/models/blob_viewer/pdf.rb
@@ -0,0 +1,12 @@
+module BlobViewer
+ class PDF < Base
+ include Rich
+ include ClientSide
+
+ self.partial_name = 'pdf'
+ self.extensions = %w(pdf)
+ self.binary = true
+ self.switcher_icon = 'file-pdf-o'
+ self.switcher_title = 'PDF'
+ end
+end
diff --git a/app/models/blob_viewer/rich.rb b/app/models/blob_viewer/rich.rb
new file mode 100644
index 00000000000..be373dbc948
--- /dev/null
+++ b/app/models/blob_viewer/rich.rb
@@ -0,0 +1,11 @@
+module BlobViewer
+ module Rich
+ extend ActiveSupport::Concern
+
+ included do
+ self.type = :rich
+ self.switcher_icon = 'file-text-o'
+ self.switcher_title = 'rendered file'
+ end
+ end
+end
diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb
new file mode 100644
index 00000000000..899107d02ea
--- /dev/null
+++ b/app/models/blob_viewer/server_side.rb
@@ -0,0 +1,11 @@
+module BlobViewer
+ module ServerSide
+ extend ActiveSupport::Concern
+
+ included do
+ self.client_side = false
+ self.max_size = 2.megabytes
+ self.absolute_max_size = 5.megabytes
+ end
+ end
+end
diff --git a/app/models/blob_viewer/simple.rb b/app/models/blob_viewer/simple.rb
new file mode 100644
index 00000000000..454a20495fc
--- /dev/null
+++ b/app/models/blob_viewer/simple.rb
@@ -0,0 +1,11 @@
+module BlobViewer
+ module Simple
+ extend ActiveSupport::Concern
+
+ included do
+ self.type = :simple
+ self.switcher_icon = 'code'
+ self.switcher_title = 'source'
+ end
+ end
+end
diff --git a/app/models/blob_viewer/sketch.rb b/app/models/blob_viewer/sketch.rb
new file mode 100644
index 00000000000..818456778e1
--- /dev/null
+++ b/app/models/blob_viewer/sketch.rb
@@ -0,0 +1,12 @@
+module BlobViewer
+ class Sketch < Base
+ include Rich
+ include ClientSide
+
+ self.partial_name = 'sketch'
+ self.extensions = %w(sketch)
+ self.binary = true
+ self.switcher_icon = 'file-image-o'
+ self.switcher_title = 'preview'
+ end
+end
diff --git a/app/models/blob_viewer/svg.rb b/app/models/blob_viewer/svg.rb
new file mode 100644
index 00000000000..b7e5cd71e6b
--- /dev/null
+++ b/app/models/blob_viewer/svg.rb
@@ -0,0 +1,12 @@
+module BlobViewer
+ class SVG < Base
+ include Rich
+ include ServerSide
+
+ self.partial_name = 'svg'
+ self.extensions = %w(svg)
+ self.binary = false
+ self.switcher_icon = 'picture-o'
+ self.switcher_title = 'image'
+ end
+end
diff --git a/app/models/blob_viewer/text.rb b/app/models/blob_viewer/text.rb
new file mode 100644
index 00000000000..e27b2c2b493
--- /dev/null
+++ b/app/models/blob_viewer/text.rb
@@ -0,0 +1,11 @@
+module BlobViewer
+ class Text < Base
+ include Simple
+ include ServerSide
+
+ self.partial_name = 'text'
+ self.binary = false
+ self.max_size = 1.megabyte
+ self.absolute_max_size = 10.megabytes
+ end
+end
diff --git a/app/models/blob_viewer/text_stl.rb b/app/models/blob_viewer/text_stl.rb
new file mode 100644
index 00000000000..8184dc0104c
--- /dev/null
+++ b/app/models/blob_viewer/text_stl.rb
@@ -0,0 +1,5 @@
+module BlobViewer
+ class TextSTL < BinarySTL
+ self.binary = false
+ end
+end
diff --git a/app/models/blob_viewer/video.rb b/app/models/blob_viewer/video.rb
new file mode 100644
index 00000000000..057f9fe516f
--- /dev/null
+++ b/app/models/blob_viewer/video.rb
@@ -0,0 +1,12 @@
+module BlobViewer
+ class Video < Base
+ include Rich
+ include ClientSide
+
+ self.partial_name = 'video'
+ self.extensions = UploaderHelper::VIDEO_EXT
+ self.binary = true
+ self.switcher_icon = 'film'
+ self.switcher_title = 'video'
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 445247f1b41..4be4aa9ffe2 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -75,29 +75,32 @@ module Ci
pipeline.update_duration
end
+ before_transition any => [:manual] do |pipeline|
+ pipeline.update_duration
+ end
+
before_transition canceled: any - [:canceled] do |pipeline|
pipeline.auto_canceled_by = nil
end
after_transition [:created, :pending] => :running do |pipeline|
- pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
+ pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
after_transition any => [:success] do |pipeline|
- pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
+ pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
after_transition [:created, :pending, :running] => :success do |pipeline|
- pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
+ pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
end
after_transition do |pipeline, transition|
next if transition.loopback?
pipeline.run_after_commit do
- PipelineHooksWorker.perform_async(id)
- Ci::ExpirePipelineCacheService.new(project, nil)
- .execute(pipeline)
+ PipelineHooksWorker.perform_async(pipeline.id)
+ ExpirePipelineCacheWorker.perform_async(pipeline.id)
end
end
@@ -385,6 +388,11 @@ module Ci
.select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
end
+ # All the merge requests for which the current pipeline runs/ran against
+ def all_merge_requests
+ @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
+ end
+
def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user)
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 8b8b3f00202..bb4cb8efd15 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -316,7 +316,7 @@ class Commit
def uri_type(path)
entry = @raw.tree.path(path)
if entry[:type] == :blob
- blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]))
+ blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
blob.image? || blob.video? ? :raw : :blob
else
entry[:type]
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 8ea95beed79..f033028c4e5 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,18 @@ 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
+
+ # Using before_update here conflicts with elasticsearch-model somehow
+ before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache?
+ before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache?
end
class_methods do
@@ -107,31 +132,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/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/routable.rb b/app/models/concerns/routable.rb
index aca99feee53..b28e05d0c28 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -163,7 +163,20 @@ module Routable
end
end
+ # Every time `project.namespace.becomes(Namespace)` is called for polymorphic_path,
+ # a new instance is instantiated, and we end up duplicating the same query to retrieve
+ # the route. Caching this per request ensures that even if we have multiple instances,
+ # we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path
+ return uncached_full_path unless RequestStore.active?
+
+ key = "routable/full_path/#{self.class.name}/#{self.id}"
+ RequestStore[key] ||= uncached_full_path
+ end
+
+ private
+
+ def uncached_full_path
if route && route.path.present?
@full_path ||= route.path
else
@@ -173,8 +186,6 @@ module Routable
end
end
- private
-
def full_name_changed?
name_changed? || parent_changed?
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 5c34844b5d3..b780c1faf81 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
- delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true
+ delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true
delegate :title, to: :note, prefix: true, allow_nil: true
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/individual_note_discussion.rb b/app/models/individual_note_discussion.rb
index c3f21c55240..6be8ca45739 100644
--- a/app/models/individual_note_discussion.rb
+++ b/app/models/individual_note_discussion.rb
@@ -10,4 +10,8 @@ class IndividualNoteDiscussion < Discussion
def individual_note?
true
end
+
+ def reply_attributes
+ super.tap { |attrs| attrs.delete(:discussion_id) }
+ end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d39ae3a6c92..305fc01f041 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -199,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 d8b0e250732..ddddb6bdf8f 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -34,6 +34,7 @@ class Label < ActiveRecord::Base
scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) }
+ scope :on_project_boards, ->(project_id) { joins(lists: :board).merge(List.movable).where(boards: { project_id: project_id }) }
def self.prioritized(project)
joins(:priorities)
diff --git a/app/models/member.rb b/app/models/member.rb
index 0545bd4eedf..7228e82e978 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -151,6 +151,27 @@ class Member < ActiveRecord::Base
member
end
+ def add_users(source, users, access_level, current_user: nil, expires_at: nil)
+ return [] unless users.present?
+
+ # Collect all user ids into separate array
+ # so we can use single sql query to get user objects
+ user_ids = users.select { |user| user =~ /\A\d+\Z/ }
+ users = users - user_ids + User.where(id: user_ids)
+
+ 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 +194,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 1d4827375d7..365fa4f1e70 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base
validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
+ validate :validate_target_project, on: :create
scope :by_source_or_target_branch, ->(branch_name) do
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
@@ -191,22 +192,23 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
end
- def diffs(diff_options = nil)
+ def diffs(diff_options = {})
if compare
- compare.diffs(diff_options)
+ # When saving MR diffs, `no_collapse` is implicitly added (because we need
+ # to save the entire contents to the DB), so add that here for
+ # consistency.
+ compare.diffs(diff_options.merge(no_collapse: true))
else
merge_request_diff.diffs(diff_options)
end
end
def diff_size
- # The `#diffs` method ends up at an instance of a class inheriting from
- # `Gitlab::Diff::FileCollection::Base`, so use those options as defaults
- # here too, to get the same diff size without performing highlighting.
- #
- opts = Gitlab::Diff::FileCollection::Base.default_options.merge(diff_options || {})
+ # Calling `merge_request_diff.diffs.real_size` will also perform
+ # highlighting, which we don't need here.
+ return real_size if merge_request_diff
- raw_diffs(opts).size
+ diffs.real_size
end
def diff_base_commit
@@ -329,6 +331,12 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def validate_target_project
+ return true if target_project.merge_requests_enabled?
+
+ errors.add :base, 'Target project has disabled merge requests'
+ end
+
def validate_fork
return true unless target_project && source_project
return true if target_project == source_project
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 6604af2b47e..f0a3c30ea74 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -260,7 +260,7 @@ class MergeRequestDiff < ActiveRecord::Base
new_attributes[:state] = :empty
else
diff_collection = compare.diffs(Commit.max_diff_options)
- new_attributes[:real_size] = compare.diffs.real_size
+ new_attributes[:real_size] = diff_collection.real_size
if diff_collection.any?
new_diffs = dump_diffs(diff_collection)
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 0bbc9451ffd..59737bb6085 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -107,7 +107,8 @@ module Network
def find_commits(skip = 0)
opts = {
max_count: self.class.max_count,
- skip: skip
+ skip: skip,
+ order: :date
}
opts[:ref] = @commit.id if @filter_ref
diff --git a/app/models/note.rb b/app/models/note.rb
index 630d0adbece..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.
diff --git a/app/models/out_of_context_discussion.rb b/app/models/out_of_context_discussion.rb
index 85794630f70..4227c40b69a 100644
--- a/app/models/out_of_context_discussion.rb
+++ b/app/models/out_of_context_discussion.rb
@@ -15,8 +15,12 @@ class OutOfContextDiscussion < Discussion
def self.override_discussion_id(note)
discussion_id(note)
end
-
+
def self.note_class
Note
end
+
+ def reply_attributes
+ super.tap { |attrs| attrs.delete(:discussion_id) }
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index a160efba912..9d64e5d406d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -74,6 +74,7 @@ class Project < ActiveRecord::Base
attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
+ attr_writer :pipeline_status
alias_attribute :title, :name
@@ -181,7 +182,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
@@ -1181,6 +1182,7 @@ class Project < ActiveRecord::Base
end
end
+ # Lazy loading of the `pipeline_status` attribute
def pipeline_status
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end
@@ -1312,6 +1314,14 @@ class Project < ActiveRecord::Base
namespace_id_changed?
end
+ def default_merge_request_target
+ if forked_from_project&.merge_requests_enabled?
+ forked_from_project
+ else
+ self
+ end
+ end
+
alias_method :name_with_namespace, :full_name
alias_method :human_name, :full_name
alias_method :path_with_namespace, :full_path
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 7bb874d7744..ba34d570dbd 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -17,9 +17,9 @@ class Repository
# same name. The cache key used by those methods must also match method's
# name.
#
- # 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 contribution_guide
+ # For example, for entry `:commit_count` there's a method called `commit_count` which
+ # stores its data in the `commit_count` cache key.
+ CACHED_METHODS = %i(size commit_count rendered_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
@@ -28,7 +28,7 @@ class Repository
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = {
- readme: :readme,
+ readme: :rendered_readme,
changelog: :changelog,
license: %i(license_blob license_key),
contributing: :contribution_guide,
@@ -450,7 +450,7 @@ class Repository
def blob_at(sha, path)
unless Gitlab::Git.blank_ref?(sha)
- Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
+ Blob.decorate(Gitlab::Git::Blob.find(self, sha, path), project)
end
rescue Gitlab::Git::Repository::NoRepository
nil
@@ -505,14 +505,8 @@ class Repository
delegate :tag_names, to: :raw_repository
cache_method :tag_names, fallback: []
- def branch_count
- branches.size
- end
+ delegate :branch_count, :tag_count, to: :raw_repository
cache_method :branch_count, fallback: 0
-
- def tag_count
- raw_repository.rugged.tags.count
- end
cache_method :tag_count, fallback: 0
def avatar
@@ -527,7 +521,11 @@ class Repository
head.readme
end
end
- cache_method :readme
+
+ def rendered_readme
+ MarkupHelper.markup_unsafe(readme.name, readme.data, project: project) if readme
+ end
+ cache_method :rendered_readme
def contribution_guide
file_on_head(:contributing)
@@ -957,15 +955,13 @@ class Repository
end
def is_ancestor?(ancestor_id, descendant_id)
- # 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
+ 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/service.rb b/app/models/service.rb
index dc76bf925d3..c71a7d169ec 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -26,6 +26,7 @@ class Service < ActiveRecord::Base
has_one :service_hook
validates :project_id, presence: true, unless: proc { |service| service.template? }
+ validates :type, presence: true
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') }
@@ -131,7 +132,7 @@ class Service < ActiveRecord::Base
end
def can_test?
- !project.empty_repo?
+ true
end
# reason why service cannot be tested
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 380835707e8..d8860718cb5 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -1,6 +1,5 @@
class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel
- include Linguist::BlobHelper
include CacheMarkdownField
include Noteable
include Participable
@@ -87,47 +86,26 @@ class Snippet < ActiveRecord::Base
]
end
- def data
- content
+ def blob
+ @blob ||= Blob.decorate(SnippetBlob.new(self), nil)
end
def hook_attrs
attributes
end
- def size
- 0
- end
-
def file_name
super.to_s
end
- # alias for compatibility with blobs and highlighting
- def path
- file_name
- end
-
- def name
- file_name
- end
-
def sanitized_file_name
file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '')
end
- def mode
- nil
- end
-
def visibility_level_field
:visibility_level
end
- def no_highlighting?
- content.lines.count > 1000
- end
-
def notes_with_associations
notes.includes(:author)
end
diff --git a/app/models/snippet_blob.rb b/app/models/snippet_blob.rb
new file mode 100644
index 00000000000..d6cab74eb1a
--- /dev/null
+++ b/app/models/snippet_blob.rb
@@ -0,0 +1,59 @@
+class SnippetBlob
+ include Linguist::BlobHelper
+
+ attr_reader :snippet
+
+ def initialize(snippet)
+ @snippet = snippet
+ end
+
+ delegate :id, to: :snippet
+
+ def name
+ snippet.file_name
+ end
+
+ alias_method :path, :name
+
+ def size
+ data.bytesize
+ end
+
+ def data
+ snippet.content
+ end
+
+ def rendered_markup
+ return unless Gitlab::MarkupHelper.gitlab_markdown?(name)
+
+ Banzai.render_field(snippet, :content)
+ end
+
+ def mode
+ nil
+ end
+
+ def binary?
+ false
+ end
+
+ def load_all_data!(repository)
+ # No-op
+ end
+
+ def lfs_pointer?
+ false
+ end
+
+ def lfs_oid
+ nil
+ end
+
+ def lfs_size
+ nil
+ end
+
+ def truncated?
+ false
+ end
+end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index da3fa7277c2..b011001b235 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -84,6 +84,10 @@ class Todo < ActiveRecord::Base
action == BUILD_FAILED
end
+ def assigned?
+ action == ASSIGNED
+ end
+
def action_name
ACTION_NAMES[action]
end
@@ -117,6 +121,14 @@ class Todo < ActiveRecord::Base
end
end
+ def self_added?
+ author == user
+ end
+
+ def self_assigned?
+ assigned? && self_added?
+ end
+
private
def keep_around_commit
diff --git a/app/models/user.rb b/app/models/user.rb
index 457ba05fb04..bd9c9f99663 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)) }
@@ -891,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
@@ -1071,11 +1068,13 @@ class User < ActiveRecord::Base
User.find_by_email(s)
end
- scope.create(
+ user = scope.build(
username: username,
email: email,
&creation_block
)
+ user.save(validate: false)
+ user
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index f8594e29547..5baac9ebe4b 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -2,20 +2,13 @@ class ProjectPolicy < BasePolicy
def rules
team_access!(user)
- owner = project.owner == user ||
- (project.group && project.group.has_owner?(user))
-
- owner_access! if user.admin? || owner
- team_member_owner_access! if owner
+ owner_access! if user.admin? || owner?
+ team_member_owner_access! if owner?
if project.public? || (project.internal? && !user.external?)
guest_access!
public_access!
-
- if project.request_access_enabled &&
- !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
- can! :request_access
- end
+ can! :request_access if access_requestable?
end
archived_access! if project.archived?
@@ -27,6 +20,13 @@ class ProjectPolicy < BasePolicy
@subject
end
+ def owner?
+ return @owner if defined?(@owner)
+
+ @owner = project.owner == user ||
+ (project.group && project.group.has_owner?(user))
+ end
+
def guest_access!
can! :read_project
can! :read_board
@@ -226,14 +226,6 @@ class ProjectPolicy < BasePolicy
disabled_features!
end
- def project_group_member?(user)
- project.group &&
- (
- project.group.members_with_parents.exists?(user_id: user.id) ||
- project.group.requesters.exists?(user_id: user.id)
- )
- end
-
def block_issues_abilities
unless project.feature_available?(:issues, user)
cannot! :read_issue if project.default_issues_tracker?
@@ -254,6 +246,22 @@ class ProjectPolicy < BasePolicy
private
+ def project_group_member?(user)
+ project.group &&
+ (
+ project.group.members_with_parents.exists?(user_id: user.id) ||
+ project.group.requesters.exists?(user_id: user.id)
+ )
+ end
+
+ def access_requestable?
+ project.request_access_enabled &&
+ !owner? &&
+ !user.admin? &&
+ !project.team.member?(user) &&
+ !project_group_member?(user)
+ end
+
# A base set of abilities for read-only users, which
# is then augmented as necessary for anonymous and other
# read-only users.
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..188c3747f18 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -1,8 +1,15 @@
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|
+ dir = 'ci_favicons'
+ dir = File.join(dir, 'dev') if Rails.env.development?
+
+ ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
+ end
end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index d5735f13c1e..e73b1a4361a 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -61,7 +61,7 @@ module Boards
if moving_to_list.movable?
moving_from_list.label_id
else
- project.boards.joins(:lists).merge(List.movable).pluck(:label_id)
+ Label.on_project_boards(project.id).pluck(:label_id)
end
Array(label_ids).compact
diff --git a/app/services/ci/expire_pipeline_cache_service.rb b/app/services/ci/expire_pipeline_cache_service.rb
deleted file mode 100644
index 91d9c1d2ba1..00000000000
--- a/app/services/ci/expire_pipeline_cache_service.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-module Ci
- class ExpirePipelineCacheService < BaseService
- attr_reader :pipeline
-
- def execute(pipeline)
- @pipeline = pipeline
- store = Gitlab::EtagCaching::Store.new
-
- store.touch(project_pipelines_path)
- store.touch(commit_pipelines_path) if pipeline.commit
- store.touch(new_merge_request_pipelines_path)
- merge_requests_pipelines_paths.each { |path| store.touch(path) }
-
- Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(@pipeline)
- end
-
- private
-
- def project_pipelines_path
- Gitlab::Routing.url_helpers.namespace_project_pipelines_path(
- project.namespace,
- project,
- format: :json)
- end
-
- def commit_pipelines_path
- Gitlab::Routing.url_helpers.pipelines_namespace_project_commit_path(
- project.namespace,
- project,
- pipeline.commit.id,
- format: :json)
- end
-
- def new_merge_request_pipelines_path
- Gitlab::Routing.url_helpers.new_namespace_project_merge_request_path(
- project.namespace,
- project,
- format: :json)
- end
-
- def merge_requests_pipelines_paths
- pipeline.merge_requests.collect do |merge_request|
- Gitlab::Routing.url_helpers.pipelines_namespace_project_merge_request_path(
- project.namespace,
- project,
- merge_request,
- format: :json)
- end
- end
- 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/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index d45da5180e1..bc0e7ad4e39 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -28,7 +28,7 @@ module MergeRequests
def find_target_project
return target_project if target_project.present? && can?(current_user, :read_project, target_project)
- project.forked_from_project || project
+ project.default_merge_request_target
end
def find_target_branch
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 7828c5806b0..535d93385e6 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -97,7 +97,8 @@ module Projects
system_hook_service.execute_hooks_for(@project, :create)
unless @project.group || @project.gitlab_project_import?
- @project.team << [current_user, :master, current_user]
+ owners = [current_user, @project.namespace.owner].compact.uniq
+ @project.add_master(owners, current_user: current_user)
end
@project.group&.refresh_members_authorized_projects
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/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index 49d45ec9dbd..6aeebc26685 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -330,6 +330,28 @@ module SlashCommands
@updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
end
+ desc 'Move issue from one column of the board to another'
+ params '~"Target column"'
+ condition do
+ issuable.is_a?(Issue) &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable) &&
+ issuable.project.boards.count == 1
+ end
+ command :board_move do |target_list_name|
+ label_ids = find_label_ids(target_list_name)
+
+ if label_ids.size == 1
+ label_id = label_ids.first
+
+ # Ensure this label corresponds to a list on the board
+ next unless Label.on_project_boards(issuable.project_id).where(id: label_id).exists?
+
+ @updates[:remove_label_ids] =
+ issuable.labels.on_project_boards(issuable.project_id).where.not(id: label_id).pluck(:id)
+ @updates[:add_label_ids] = [label_id]
+ end
+ end
+
def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
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
index 9a0a5a12f91..363135ef09b 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -6,18 +6,16 @@ module Users
@params = params.dup
end
- def execute
- raise Gitlab::Access::AccessDeniedError unless can_create_user?
+ def execute(skip_authorization: false)
+ raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_create_user?
- user = User.new(build_user_params)
+ user_params = build_user_params(skip_authorization: skip_authorization)
+ user = User.new(user_params)
if current_user&.admin?
- if params[:reset_password]
- user.generate_reset_token
- params[:force_random_password] = true
- end
+ @reset_token = user.generate_reset_token if params[:reset_password]
- if params[:force_random_password]
+ if user_params[:force_random_password]
random_password = Devise.friendly_token.first(Devise.password_length.min)
user.password = user.password_confirmation = random_password
end
@@ -81,7 +79,7 @@ module Users
]
end
- def build_user_params
+ def build_user_params(skip_authorization:)
if current_user&.admin?
user_params = params.slice(*admin_create_params)
user_params[:created_by_id] = current_user&.id
@@ -90,11 +88,20 @@ module Users
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
+ allowed_signup_params = signup_params
+ allowed_signup_params << :skip_confirmation if skip_authorization
+
+ user_params = params.slice(*allowed_signup_params)
+ if user_params[:skip_confirmation].nil?
+ user_params[:skip_confirmation] = skip_user_confirmation_email_from_setting
+ end
end
user_params
end
+
+ def skip_user_confirmation_email_from_setting
+ !current_application_settings.send_user_confirmation_email
+ end
end
end
diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb
index a2105d31f71..e22f7225ae2 100644
--- a/app/services/users/create_service.rb
+++ b/app/services/users/create_service.rb
@@ -6,8 +6,8 @@ module Users
@params = params.dup
end
- def execute
- user = Users::BuildService.new(current_user, params).execute
+ def execute(skip_authorization: false)
+ user = Users::BuildService.new(current_user, params).execute(skip_authorization: skip_authorization)
@reset_token = user.generate_reset_token if user.recently_sent_password_reset?
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 f4ba44096d3..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
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..be8644c0ca6
--- /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/settings/usage_statistics', anchor: 'usage-ping'), 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/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 8c9fdc9ae42..53f0a1e7fde 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -73,6 +73,12 @@
= container_reg
%span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled
+ - gitlab_pages = 'GitLab Pages'
+ - gitlab_pages_enabled = Gitlab.config.pages.enabled
+ %p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") }
+ = gitlab_pages
+ %span.light.pull-right
+ = boolean_to_icon gitlab_pages_enabled
.col-md-4
%h4
diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml
new file mode 100644
index 00000000000..6217d5fb135
--- /dev/null
+++ b/app/views/admin/hooks/_form.html.haml
@@ -0,0 +1,40 @@
+= form_errors(hook)
+
+.form-group
+ = form.label :url, 'URL', class: 'control-label'
+ .col-sm-10
+ = form.text_field :url, class: 'form-control'
+.form-group
+ = form.label :token, 'Secret Token', class: 'control-label'
+ .col-sm-10
+ = form.text_field :token, class: 'form-control'
+ %p.help-block
+ Use this token to validate received payloads
+.form-group
+ = form.label :url, 'Trigger', class: 'control-label'
+ .col-sm-10.prepend-top-10
+ %div
+ System hook will be triggered on set of events like creating project
+ or adding ssh key. But you can also enable extra triggers like Push events.
+
+ .prepend-top-default
+ = form.check_box :push_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :push_events, class: 'list-label' do
+ %strong Push events
+ %p.light
+ This url will be triggered by a push to the repository
+ %div
+ = form.check_box :tag_push_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :tag_push_events, class: 'list-label' do
+ %strong Tag push events
+ %p.light
+ This url will be triggered when a new tag is pushed to the repository
+.form-group
+ = form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox'
+ .col-sm-10
+ .checkbox
+ = form.label :enable_ssl_verification do
+ = form.check_box :enable_ssl_verification
+ %strong Enable SSL verification
diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml
new file mode 100644
index 00000000000..0777f5e2629
--- /dev/null
+++ b/app/views/admin/hooks/edit.html.haml
@@ -0,0 +1,14 @@
+- page_title 'Edit System Hook'
+%h3.page-title
+ Edit System Hook
+
+%p.light
+ #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
+ used for binding events when GitLab creates a User or Project.
+
+%hr
+
+= form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f|
+ = render partial: 'form', locals: { form: f, hook: @hook }
+ .form-actions
+ = f.submit 'Save changes', class: 'btn btn-create'
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index d9c7948763a..71117758921 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -1,57 +1,17 @@
-- page_title "System Hooks"
+- page_title 'System Hooks'
%h3.page-title
System hooks
%p.light
- #{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be
+ #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
used for binding events when GitLab creates a User or Project.
%hr
-
= form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f|
- = form_errors(@hook)
-
- .form-group
- = f.label :url, 'URL', class: 'control-label'
- .col-sm-10
- = f.text_field :url, class: 'form-control'
- .form-group
- = f.label :token, 'Secret Token', class: 'control-label'
- .col-sm-10
- = f.text_field :token, class: 'form-control'
- %p.help-block
- Use this token to validate received payloads
- .form-group
- = f.label :url, "Trigger", class: 'control-label'
- .col-sm-10.prepend-top-10
- %div
- System hook will be triggered on set of events like creating project
- or adding ssh key. But you can also enable extra triggers like Push events.
-
- .prepend-top-default
- = f.check_box :push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :push_events, class: 'list-label' do
- %strong Push events
- %p.light
- This url will be triggered by a push to the repository
- %div
- = f.check_box :tag_push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :tag_push_events, class: 'list-label' do
- %strong Tag push events
- %p.light
- This url will be triggered when a new tag is pushed to the repository
- .form-group
- = f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox'
- .col-sm-10
- .checkbox
- = f.label :enable_ssl_verification do
- = f.check_box :enable_ssl_verification
- %strong Enable SSL verification
+ = render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
- = f.submit "Add system hook", class: "btn btn-create"
+ = f.submit 'Add system hook', class: 'btn btn-create'
%hr
- if @hooks.any?
@@ -62,11 +22,12 @@
- @hooks.each do |hook|
%li
.controls
- = link_to 'Test hook', admin_hook_test_path(hook), class: "btn btn-sm"
- = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm"
+ = link_to 'Test hook', test_admin_hook_path(hook), class: 'btn btn-sm'
+ = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
+ = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.monospace= hook.url
%div
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray= trigger.titleize
- %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
+ %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 0e848386ebb..4594c52b34b 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -2,10 +2,10 @@
%ul.nav-links
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: 'Your groups' do
- Your Groups
+ Your groups
= nav_link(page: explore_groups_path) do
- = link_to explore_groups_path, title: 'Explore groups' do
- Explore Groups
+ = link_to explore_groups_path, title: 'Explore public groups' do
+ Explore public groups
.nav-controls
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index d0c12aa57ae..38fd053ae65 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -9,7 +9,7 @@
.title-item.author-name
- if todo.author
- = link_to_author(todo)
+ = link_to_author(todo, self_added: todo.self_added?)
- else
(removed)
@@ -22,6 +22,10 @@
- else
(removed)
+ - if todo.self_assigned?
+ .title-item.action-name
+ to yourself
+
.title-item
&middot;
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index 158061579f6..e2aec532a9d 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -8,6 +8,7 @@ xml.entry do
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
xml.author do
+ xml.username event.author_username
xml.name event.author_name
xml.email event.author_public_email
end
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index bb2cd0d44c8..ffe07b217a7 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -7,6 +7,15 @@
= render 'explore/head'
= render 'nav'
+- if cookies[:explore_groups_landing_dismissed] != 'true'
+ .explore-groups.landing.content-block.js-explore-groups-landing.hidden
+ %button.dismiss-button{ type: 'button', 'aria-label' => 'Dismiss' }= icon('times')
+ .svg-container
+ = custom_icon('icon_explore_groups_splash')
+ .inner-content
+ %p Below you will find all the groups that are public.
+ %p You can easily contribute to them by requesting to join these groups.
+
- if @groups.present?
= render 'groups'
- else
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/index.html.haml b/app/views/help/index.html.haml
index f93b6b63426..b20e3a22133 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -27,8 +27,7 @@
.row
.col-md-8
.documentation-index
- = preserve do
- = markdown(@help_index)
+ = markdown(@help_index)
.col-md-4
.panel.panel-default
.panel-heading
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/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index e34cddeb3e2..8ab9747efc5 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -11,13 +11,13 @@
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
@@ -56,7 +56,7 @@
Snippets
- if project_nav_tab? :settings
- = nav_link(path: %w[projects#edit members#show integrations#show repository#show ci_cd#show pages#show]) do
+ = nav_link(path: %w[projects#edit members#show integrations#show services#edit repository#show ci_cd#show pages#show]) do
= link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
%span
Settings
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index c6b1db17f91..02eb7c8462c 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -74,7 +74,7 @@
- else
%hr
- blob = diff_file.blob
- - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
+ - if blob && blob.readable_text?
%table.code.white
= render partial: "projects/diffs/line", collection: diff_file.highlighted_diff_lines, as: :line, locals: { diff_file: diff_file, plain: true, email: true }
- else
diff --git a/app/views/projects/_fork_suggestion.html.haml b/app/views/projects/_fork_suggestion.html.haml
new file mode 100644
index 00000000000..c855bfaf067
--- /dev/null
+++ b/app/views/projects/_fork_suggestion.html.haml
@@ -0,0 +1,11 @@
+- 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/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index e1fea8ccf3d..df3b1c75508 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -1,10 +1,9 @@
-
- ref = local_assigns.fetch(:ref)
- status = commit.status(ref)
- if status
= link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status)
- = ci_label_for_status(status)
+ = ci_text_for_status(status)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index b6fb08b68e9..c0d12cbc66e 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -4,8 +4,7 @@
- if can?(current_user, :push_code, @project)
= link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme'
.file-content.wiki
- = cache(readme_cache_key) do
- = render_readme(readme)
+ = markup(readme.name, readme.data, rendered: @repository.rendered_readme)
- else
.row-content-block.second-block.center
%h3.page-title
diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml
index 41d42740f61..2bab22e125d 100644
--- a/app/views/projects/_wiki.html.haml
+++ b/app/views/projects/_wiki.html.haml
@@ -2,8 +2,7 @@
%div{ class: container_class }
.wiki-holder.prepend-top-default.append-bottom-default
.wiki
- = preserve do
- = render_wiki_content(@wiki_home)
+ = render_wiki_content(@wiki_home)
- else
- can_create_wiki = can?(current_user, :create_wiki, @project)
.project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] }
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 9aafff343f0..3f12d64d044 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -26,9 +26,4 @@
%article.file-holder
= render "projects/blob/header", blob: blob
- - if blob.empty?
- .file-content.code
- .nothing-here-block
- Empty file
- - else
- = render blob.to_partial_path(@project), blob: blob
+ = render 'projects/blob/content', blob: blob
diff --git a/app/views/projects/blob/_content.html.haml b/app/views/projects/blob/_content.html.haml
new file mode 100644
index 00000000000..7afbd85cd6d
--- /dev/null
+++ b/app/views/projects/blob/_content.html.haml
@@ -0,0 +1,8 @@
+- simple_viewer = blob.simple_viewer
+- rich_viewer = blob.rich_viewer
+- rich_viewer_active = rich_viewer && params[:viewer] != 'simple'
+
+= render 'projects/blob/viewer', viewer: simple_viewer, hidden: rich_viewer_active
+
+- if rich_viewer
+ = render 'projects/blob/viewer', viewer: rich_viewer, hidden: !rich_viewer_active
diff --git a/app/views/projects/blob/_download.html.haml b/app/views/projects/blob/_download.html.haml
deleted file mode 100644
index 7908fcae3de..00000000000
--- a/app/views/projects/blob/_download.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-.file-content.blob_file.blob-no-preview
- .center
- = link_to namespace_project_raw_path(@project.namespace, @project, @id) do
- %h1.light
- %i.fa.fa-download
- %h4
- Download (#{number_to_human_size blob_size(blob)})
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 7a4a293548c..219dc14645b 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -9,17 +9,19 @@
= copy_file_path_button(blob.path)
%small
- = number_to_human_size(blob_size(blob))
+ = number_to_human_size(blob.raw_size)
.file-actions.hidden-xs
+ = render 'projects/blob/viewer_switcher', blob: blob unless blame
+
.btn-group{ role: "group" }<
- = copy_blob_content_button(blob) if !blame && blob_rendered_as_text?(blob)
- = open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id))
+ = copy_blob_source_button(blob) unless blame
+ = open_raw_blob_button(blob)
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }<
-# only show normal/blame view links for text files
- - if blob_text_viewable?(blob)
+ - if blob.readable_text?
- if blame
= link_to 'Normal view', namespace_project_blob_path(@project.namespace, @project, @id),
class: 'btn btn-sm'
@@ -34,14 +36,9 @@
tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
.btn-group{ role: "group" }<
- = edit_blob_link if blob_text_viewable?(blob)
+ = edit_blob_link if blob.readable_text?
- 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 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 'projects/fork_suggestion'
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
deleted file mode 100644
index 73877d730f5..00000000000
--- a/app/views/projects/blob/_image.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-.file-content.image_file
- %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
index 4ee4b03ff04..0090f7a11df 100644
--- a/app/views/projects/blob/_markup.html.haml
+++ b/app/views/projects/blob/_markup.html.haml
@@ -1,4 +1,4 @@
- blob.load_all_data!(@repository)
.file-content.wiki
- = render_markup(blob.name, blob.data)
+ = markup(blob.name, blob.data)
diff --git a/app/views/projects/blob/_render_error.html.haml b/app/views/projects/blob/_render_error.html.haml
new file mode 100644
index 00000000000..9eef6cafd04
--- /dev/null
+++ b/app/views/projects/blob/_render_error.html.haml
@@ -0,0 +1,7 @@
+.file-content.code
+ .nothing-here-block
+ The #{viewer.switcher_title} could not be displayed because #{blob_render_error_reason(viewer)}.
+
+ You can
+ = blob_render_error_options(viewer).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe
+ instead.
diff --git a/app/views/projects/blob/_svg.html.haml b/app/views/projects/blob/_svg.html.haml
deleted file mode 100644
index 93be58fc658..00000000000
--- a/app/views/projects/blob/_svg.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- 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
deleted file mode 100644
index 20638f6961d..00000000000
--- a/app/views/projects/blob/_text.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- 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
deleted file mode 100644
index a505f87df40..00000000000
--- a/app/views/projects/blob/_too_large.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-.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/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
new file mode 100644
index 00000000000..5326bb3e0cf
--- /dev/null
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -0,0 +1,14 @@
+- hidden = local_assigns.fetch(:hidden, false)
+- render_error = viewer.render_error
+- load_asynchronously = local_assigns.fetch(:load_asynchronously, viewer.server_side?) && render_error.nil?
+
+- url = url_for(params.merge(viewer: viewer.type, format: :json)) if load_asynchronously
+.blob-viewer{ data: { type: viewer.type, url: url }, class: ('hidden' if hidden) }
+ - if load_asynchronously
+ .text-center.prepend-top-default.append-bottom-default
+ = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content')
+ - elsif render_error
+ = render 'projects/blob/render_error', viewer: viewer
+ - else
+ - viewer.prepare!
+ = render viewer.partial_path, viewer: viewer
diff --git a/app/views/projects/blob/_viewer_switcher.html.haml b/app/views/projects/blob/_viewer_switcher.html.haml
new file mode 100644
index 00000000000..6a521069418
--- /dev/null
+++ b/app/views/projects/blob/_viewer_switcher.html.haml
@@ -0,0 +1,12 @@
+- if blob.show_viewer_switcher?
+ - simple_viewer = blob.simple_viewer
+ - rich_viewer = blob.rich_viewer
+
+ .btn-group.js-blob-viewer-switcher{ role: "group" }
+ - simple_label = "Display #{simple_viewer.switcher_title}"
+ %button.btn.btn-default.btn-sm.js-blob-viewer-switch-btn.has-tooltip{ 'aria-label' => simple_label, title: simple_label, data: { viewer: 'simple', container: 'body' } }>
+ = icon(simple_viewer.switcher_icon)
+
+ - rich_label = "Display #{rich_viewer.switcher_title}"
+ %button.btn.btn-default.btn-sm.js-blob-viewer-switch-btn.has-tooltip{ 'aria-label' => rich_label, title: rich_label, data: { viewer: 'rich', container: 'body' } }>
+ = icon(rich_viewer.switcher_icon)
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/preview.html.haml b/app/views/projects/blob/preview.html.haml
index 5cafb644b40..e87b73c9a34 100644
--- a/app/views/projects/blob/preview.html.haml
+++ b/app/views/projects/blob/preview.html.haml
@@ -1,12 +1,8 @@
.diff-file
.diff-content
- - if gitlab_markdown?(@blob.name)
+ - if markup?(@blob.name)
.file-content.wiki
- = preserve do
- = markdown(@content)
- - elsif markup?(@blob.name)
- .file-content.wiki
- = raw render_markup(@blob.name, @content)
+ = markup(@blob.name, @content)
- else
.file-content.code.js-syntax-highlight
- unless @diff_lines.empty?
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index b9b3f3ec7a3..67f57b5e4b9 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -2,6 +2,9 @@
- page_title @blob.path, @ref
= render "projects/commits/head"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('blob')
+
%div{ class: container_class }
= render 'projects/last_push'
diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml
new file mode 100644
index 00000000000..684240d02c7
--- /dev/null
+++ b/app/views/projects/blob/viewers/_download.html.haml
@@ -0,0 +1,7 @@
+.file-content.blob_file.blob-no-preview
+ .center
+ = link_to blob_raw_url do
+ %h1.light
+ = icon('download')
+ %h4
+ Download (#{number_to_human_size(viewer.blob.raw_size)})
diff --git a/app/views/projects/blob/viewers/_empty.html.haml b/app/views/projects/blob/viewers/_empty.html.haml
new file mode 100644
index 00000000000..a293a8de231
--- /dev/null
+++ b/app/views/projects/blob/viewers/_empty.html.haml
@@ -0,0 +1,3 @@
+.file-content.code
+ .nothing-here-block
+ Empty file
diff --git a/app/views/projects/blob/viewers/_image.html.haml b/app/views/projects/blob/viewers/_image.html.haml
new file mode 100644
index 00000000000..640d59b3174
--- /dev/null
+++ b/app/views/projects/blob/viewers/_image.html.haml
@@ -0,0 +1,2 @@
+.file-content.image_file
+ %img{ src: blob_raw_url, alt: viewer.blob.name }
diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml
new file mode 100644
index 00000000000..230305b488d
--- /dev/null
+++ b/app/views/projects/blob/viewers/_markup.html.haml
@@ -0,0 +1,4 @@
+- blob = viewer.blob
+- rendered_markup = blob.rendered_markup if blob.respond_to?(:rendered_markup)
+.file-content.wiki
+ = markup(blob.name, blob.data, rendered: rendered_markup)
diff --git a/app/views/projects/blob/_notebook.html.haml b/app/views/projects/blob/viewers/_notebook.html.haml
index ab1cf933944..2399fb16265 100644
--- a/app/views/projects/blob/_notebook.html.haml
+++ b/app/views/projects/blob/viewers/_notebook.html.haml
@@ -2,4 +2,4 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('notebook_viewer')
-.file-content#js-notebook-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
+.file-content#js-notebook-viewer{ data: { endpoint: blob_raw_url } }
diff --git a/app/views/projects/blob/_pdf.html.haml b/app/views/projects/blob/viewers/_pdf.html.haml
index 58dc88e3bf7..1dd179c4fdc 100644
--- a/app/views/projects/blob/_pdf.html.haml
+++ b/app/views/projects/blob/viewers/_pdf.html.haml
@@ -2,4 +2,4 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pdf_viewer')
-.file-content#js-pdf-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
+.file-content#js-pdf-viewer{ data: { endpoint: blob_raw_url } }
diff --git a/app/views/projects/blob/_sketch.html.haml b/app/views/projects/blob/viewers/_sketch.html.haml
index dad9369cb2a..49f716c2c59 100644
--- a/app/views/projects/blob/_sketch.html.haml
+++ b/app/views/projects/blob/viewers/_sketch.html.haml
@@ -2,6 +2,6 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sketch_viewer')
-.file-content#js-sketch-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
+.file-content#js-sketch-viewer{ data: { endpoint: blob_raw_url } }
.js-loading-icon.text-center.prepend-top-default.append-bottom-default.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' }
= icon('spinner spin 2x', 'aria-hidden' => 'true');
diff --git a/app/views/projects/blob/_stl.html.haml b/app/views/projects/blob/viewers/_stl.html.haml
index a9332a0eeb6..e4e9d746176 100644
--- a/app/views/projects/blob/_stl.html.haml
+++ b/app/views/projects/blob/viewers/_stl.html.haml
@@ -2,7 +2,7 @@
= page_specific_javascript_bundle_tag('stl_viewer')
.file-content.is-stl-loading
- .text-center#js-stl-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
+ .text-center#js-stl-viewer{ data: { endpoint: blob_raw_url } }
= icon('spinner spin 2x', class: 'prepend-top-default append-bottom-default', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
.text-center.prepend-top-default.append-bottom-default.stl-controls
.btn-group
diff --git a/app/views/projects/blob/viewers/_svg.html.haml b/app/views/projects/blob/viewers/_svg.html.haml
new file mode 100644
index 00000000000..62f647581b6
--- /dev/null
+++ b/app/views/projects/blob/viewers/_svg.html.haml
@@ -0,0 +1,4 @@
+- blob = viewer.blob
+- data = sanitize_svg_data(blob.data)
+.file-content.image_file
+ %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(data)}", alt: blob.name }
diff --git a/app/views/projects/blob/viewers/_text.html.haml b/app/views/projects/blob/viewers/_text.html.haml
new file mode 100644
index 00000000000..a91df321ca0
--- /dev/null
+++ b/app/views/projects/blob/viewers/_text.html.haml
@@ -0,0 +1 @@
+= render 'shared/file_highlight', blob: viewer.blob, repository: @repository
diff --git a/app/views/projects/blob/viewers/_video.html.haml b/app/views/projects/blob/viewers/_video.html.haml
new file mode 100644
index 00000000000..595a890a27d
--- /dev/null
+++ b/app/views/projects/blob/viewers/_video.html.haml
@@ -0,0 +1,2 @@
+.file-content.video
+ %video{ src: blob_raw_url, controls: true, data: { setup: '{}' } }
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
index 008d1186478..190e7290303 100644
--- a/app/views/projects/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -22,7 +22,7 @@
Milestone
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable
- = dropdown_title("Assignee milestone")
+ = dropdown_title("Assign milestone")
= dropdown_filter("Search milestones")
= dropdown_content
= dropdown_loading
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/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/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index 438a98c3e95..c781e423c4d 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -3,9 +3,9 @@
- return unless blob.respond_to?(:text?)
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
- - elsif blob.only_display_raw?
+ - elsif blob.too_large?
.nothing-here-block The file could not be displayed because it is too large.
- - elsif blob_text_viewable?(blob)
+ - elsif blob.readable_text?
- if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.collapsed?
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 4b49bed835f..71a1b9e6c05 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -27,7 +27,7 @@
- diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit)
- next unless blob
- - blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw?
+ - blob.load_all_data!(diffs.project.repository) unless blob.too_large?
- file_hash = hexdigest(diff_file.file_path)
= render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 0232a09b4a8..f22b385fc0f 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -6,7 +6,7 @@
- unless diff_file.submodule?
.file-actions.hidden-xs
- - if blob_text_viewable?(blob)
+ - if blob.readable_text?
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment')
\
@@ -18,4 +18,6 @@
= view_file_button(diff_commit.id, diff_file.new_path, project)
= view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment
+ = render 'projects/fork_suggestion'
+
= render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 85e442e115c..50e0bad3ccf 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -60,7 +60,7 @@
git init
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add .
- git commit
+ git commit -m "Initial commit"
git push -u origin master
%fieldset
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/hooks/_index.html.haml b/app/views/projects/hooks/_index.html.haml
index 8faad351463..676b7c345bc 100644
--- a/app/views/projects/hooks/_index.html.haml
+++ b/app/views/projects/hooks/_index.html.haml
@@ -1 +1,23 @@
-= render 'shared/web_hooks/form', hook: @hook, hooks: @hooks, url_components: [@project.namespace.becomes(Namespace), @project]
+.row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ = page_title
+ %p
+ #{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be
+ used for binding events when something is happening within the project.
+
+ .col-lg-9.append-bottom-default
+ = form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f|
+ = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
+ = f.submit 'Add webhook', class: 'btn btn-create'
+
+ %hr
+ %h5.prepend-top-default
+ Webhooks (#{@hooks.count})
+ - if @hooks.any?
+ %ul.well-list
+ - @hooks.each do |hook|
+ = render 'project_hook', hook: hook
+ - else
+ %p.settings-message.text-center.append-bottom-0
+ No webhooks found, add one in the form above.
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
new file mode 100644
index 00000000000..7998713be1f
--- /dev/null
+++ b/app/views/projects/hooks/edit.html.haml
@@ -0,0 +1,14 @@
+= render 'projects/settings/head'
+
+.row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ = page_title
+ %p
+ #{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be
+ used for binding events when something is happening within the project.
+ .col-lg-9.append-bottom-default
+ = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hook_path do |f|
+ = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
+ = f.submit 'Save changes', class: 'btn btn-create'
+
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index fcbd8829595..2a871966aa8 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -50,7 +50,7 @@
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
.issue-details.issuable-details
- .detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) }
+ .detail-page-description.content-block
.issue-title-data.hidden{ "data" => { "initial-title" => markdown_field(@issue, :title),
"endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
} }
@@ -58,8 +58,7 @@
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki
- = preserve do
- = markdown_field(@issue, :description)
+ = markdown_field(@issue, :description)
%textarea.hidden.js-task-list-field
= @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 8d134aaac67..9cf24e10842 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -38,7 +38,7 @@
.panel-heading
Target branch
.panel-body.clearfix
- - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project]
+ - projects = target_projects(@project)
.merge-request-select.dropdown
= f.hidden_field :target_project_id
= dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
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/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index 683cb8a5a27..8a390cf8700 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -6,8 +6,7 @@
- if @merge_request.description.present?
.description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' }
.wiki
- = preserve do
- = markdown_field(@merge_request, :description)
+ = markdown_field(@merge_request, :description)
%textarea.hidden.js-task-list-field
= @merge_request.description
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/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/_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 e57a76dbfd2..1e66c6079e3 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -5,7 +5,7 @@
%div{ class: container_class }
%h3.page-title
- Edit Milestone #{@milestone.to_reference}
+ Edit Milestone
%hr
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index a173117984d..4b692aba11c 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -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
@@ -36,15 +36,14 @@
%a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
- .detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) }
+ .detail-page-description.milestone-detail
%h2.title
= markdown_field(@milestone, :title)
%div
- if @milestone.description.present?
.description
.wiki
- = preserve do
- = markdown_field(@milestone, :description)
+ = markdown_field(@milestone, :description)
- if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero?
.alert.alert-success.prepend-top-default
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 7cf604bb772..7afccb3900a 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -75,8 +75,7 @@
= icon('trash-o', class: 'danger-highlight')
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md
- = preserve do
- = note.redacted_note_html
+ = note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
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/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index 50ed78286d2..0f1a76a104a 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -1,2 +1,3 @@
- page_title @service.title, "Services"
+= render "projects/settings/head"
= render 'form'
diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml
index 88bcb541dac..5a5ade03624 100644
--- a/app/views/projects/settings/_head.html.haml
+++ b/app/views/projects/settings/_head.html.haml
@@ -14,7 +14,7 @@
%span
Members
- if can_edit
- = nav_link(controller: :integrations) do
+ = nav_link(controller: [:integrations, :services, :hooks]) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span
Integrations
diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index ceabe2eab3d..8dc276a3bec 100644
--- a/app/views/projects/settings/integrations/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
@@ -9,6 +9,7 @@
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
%span.append-right-10.inline
SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
+ = link_to "Edit", edit_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm"
= link_to "Test", test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm"
= link_to namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent" do
%span.sr-only Remove
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index fd7bd21677c..d6c4195e2d0 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -70,7 +70,7 @@
= 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
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 7c6be003d4c..7a175f63eeb 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -4,7 +4,7 @@
.project-snippets
%article.file-holder.snippet-file-content
- = render 'shared/snippets/blob', raw_path: raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
+ = render 'shared/snippets/blob'
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 451e011a4b8..4c4f3655b97 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -24,8 +24,7 @@
- if release && release.description.present?
.description.prepend-top-default
.wiki
- = preserve do
- = markdown_field(release, :description)
+ = markdown_field(release, :description)
.row-fixed-content.controls
= render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 1c4135c8a54..e996ae3e4fc 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -38,7 +38,6 @@
- if @release.description.present?
.description
.wiki
- = preserve do
- = markdown_field(@release, :description)
+ = markdown_field(@release, :description)
- else
This tag has no release notes.
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index bdcc160a067..01599060844 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -5,4 +5,4 @@
%strong
= readme.name
.file-content.wiki
- = render_readme(readme)
+ = markup(readme.name, readme.data)
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 259207a6dfd..e7b3fe3ffda 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -1,3 +1,7 @@
+.tree-controls
+ = render 'projects/find_file_link'
+ = render 'projects/buttons/download', project: @project, ref: @ref
+
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index a2a26039220..910d765aed0 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -7,12 +7,4 @@
= render 'projects/last_push'
%div{ class: container_class }
- .tree-controls
- = render 'projects/find_file_link'
- = render 'projects/buttons/download', project: @project, ref: @ref
-
- #tree-holder.tree-holder.clearfix
- .nav-block
- = render 'projects/tree/tree_header', tree: @tree
-
- = render 'projects/tree/tree_content', tree: @tree
+ = render 'projects/files'
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/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/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 3609461b721..c00967546aa 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -27,7 +27,6 @@
.wiki-holder.prepend-top-default.append-bottom-default
.wiki
- = preserve do
- = render_wiki_content(@page)
+ = render_wiki_content(@page)
= render 'sidebar'
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index fc4385865a4..b4bc8982c05 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -8,7 +8,6 @@
.pull-right ##{issue.iid}
- if issue.description.present?
.description.term
- = preserve do
- = search_md_sanitize(issue, :description)
+ = search_md_sanitize(issue, :description)
%span.light
#{issue.project.name_with_namespace}
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 9b583285d02..1a5499e4d58 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -9,7 +9,6 @@
.pull-right= merge_request.to_reference
- if merge_request.description.present?
.description.term
- = preserve do
- = search_md_sanitize(merge_request, :description)
+ = search_md_sanitize(merge_request, :description)
%span.light
#{merge_request.project.name_with_namespace}
diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml
index 9664f65a36e..2daa96e34d1 100644
--- a/app/views/search/results/_milestone.html.haml
+++ b/app/views/search/results/_milestone.html.haml
@@ -5,5 +5,4 @@
- if milestone.description.present?
.description.term
- = preserve do
- = search_md_sanitize(milestone, :description)
+ = search_md_sanitize(milestone, :description)
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index f3701b89bb4..a7e178dfa71 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -22,5 +22,4 @@
.note-search-result
.term
- = preserve do
- = search_md_sanitize(note, :note)
+ = search_md_sanitize(note, :note)
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index f84be600df8..c4a5131c1a7 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -21,7 +21,7 @@
.file-content.wiki
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
- = render_markup(snippet.file_name, chunk[:data])
+ = markup(snippet.file_name, chunk[:data])
- else
.file-content.code
.nothing-here-block Empty file
@@ -39,7 +39,7 @@
.blob-content
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
- = highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.no_highlighting?)
+ = highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.blob.no_highlighting?)
- else
.file-content.code
.nothing-here-block Empty file
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/_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/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 9c5053dace5..b200e5fc528 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -4,8 +4,7 @@
= render "projects/services/#{@service.to_param}/help", subject: subject
- elsif @service.help.present?
.well
- = preserve do
- = markdown @service.help
+ = markdown @service.help
.service-settings
.form-group
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/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_explore_groups_splash.svg b/app/views/shared/icons/_icon_explore_groups_splash.svg
new file mode 100644
index 00000000000..79f17872739
--- /dev/null
+++ b/app/views/shared/icons/_icon_explore_groups_splash.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="62" height="50" viewBox="260 141 62 50" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M24.6 7.7H56c3.3 0 6 2.7 6 6V44c0 3.3-2.7 6-6 6H6c-3.3 0-6-2.7-6-6V4.8C0 2 2.2 0 4.8 0h12c1.5 0 3 1 4 2l3.8 5.7z"/><mask id="e" width="62" height="50" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M4.2 13c3.7 0 4-1.7 4-4.5S7 4.8 4.2 4.8 0 5.8 0 8.5C0 11.3.5 13 4.2 13z"/><mask id="f" width="10.7" height="10.7" x="-1.2" y="-1.2"><path fill="#fff" d="M-1.2 3.6H9.5v10.7H-1.2z"/><use xlink:href="#b"/></mask><path id="c" d="M4.2 13c3.7 0 4-1.7 4-4.5S7 4.8 4.2 4.8 0 5.8 0 8.5C0 11.3.5 13 4.2 13z"/><mask id="g" width="10.7" height="10.7" x="-1.2" y="-1.2"><path fill="#fff" d="M-1.2 3.6H9.5v10.7H-1.2z"/><use xlink:href="#c"/></mask><path id="d" d="M5.4 16c4.7 0 5.3-2.3 5.3-6 0-3.5-1.7-4.6-5.3-4.6C1.7 5.4 0 6.4 0 10s.6 6 5.4 6z"/><mask id="h" width="13.1" height="13.1" x="-1.2" y="-1.2"><path fill="#fff" d="M-1.2 4.2h13v13H-1z"/><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(260 141)"><use fill="#FFF" stroke="#EEE" stroke-width="4.8" mask="url(#e)" xlink:href="#a"/><g transform="translate(33.98 22.62)"><use fill="#B5A7DD" xlink:href="#b"/><use stroke="#FFF" stroke-width="2.4" mask="url(#f)" xlink:href="#b"/><ellipse cx="4.2" cy="3" fill="#B5A7DD" stroke="#FFF" stroke-width="1.2" rx="3" ry="3"/></g><g transform="translate(19.673 22.62)"><use fill="#B5A7DD" xlink:href="#c"/><use stroke="#FFF" stroke-width="2.4" mask="url(#g)" xlink:href="#c"/><ellipse cx="4.2" cy="3" fill="#B5A7DD" stroke="#FFF" stroke-width="1.2" rx="3" ry="3"/></g><g transform="translate(25.635 21.43)"><use fill="#B5A7DD" xlink:href="#d"/><use stroke="#FFF" stroke-width="2.4" mask="url(#h)" xlink:href="#d"/><ellipse cx="5.4" cy="3.6" fill="#B5A7DD" stroke="#FFF" stroke-width="1.2" rx="3.6" ry="3.6"/></g></g></svg>
diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg
index 2daa55a8652..5468545da2e 100644
--- a/app/views/shared/icons/_mr_bold.svg
+++ b/app/views/shared/icons/_mr_bold.svg
@@ -1 +1,2 @@
-<svg width="15" height="20" viewBox="0 0 12 14" xmlns="http://www.w3.org/2000/svg"><path d="M1 4.967a2.15 2.15 0 1 1 2.3 0v5.066a2.15 2.15 0 1 1-2.3 0V4.967zm7.85 5.17V5.496c0-.745-.603-1.346-1.35-1.346V6l-3-3 3-3v1.85c2.016 0 3.65 1.63 3.65 3.646v4.45a2.15 2.15 0 1 1-2.3.191z" fill-rule="nonzero"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg>
+
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index c72268473ca..1a12f110945 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -21,7 +21,7 @@
- if params[:assignee_id].present?
= hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter
= render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
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/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 4c7d69d40d5..5247d6a51e6 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -1,11 +1,14 @@
-# @project is present when viewing Project's milestone
- project = @project || issuable.project
+- namespace = @project_namespace || project.namespace.becomes(Namespace)
- assignee = issuable.assignee
- issuable_type = issuable.class.table_name
-- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
+- base_url_args = [namespace, project]
+- issuable_type_args = base_url_args + [issuable_type]
+- issuable_url_args = base_url_args + [issuable]
- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable)
-%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable) }
+%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable_url_args) }
%span
- if show_project_name
%strong #{project.name} &middot;
@@ -13,17 +16,17 @@
%strong #{project.name_with_namespace} &middot;
- if issuable.is_a?(Issue)
= confidential_icon(issuable)
- = link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
+ = link_to_gfm issuable.title, issuable_url_args, title: issuable.title
.issuable-detail
= link_to [project.namespace.becomes(Namespace), project, issuable] do
%span.issuable-number= issuable.to_reference
- issuable.labels.each do |label|
- = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
+ = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
- render_colored_label(label)
%span.assignee-icon
- if assignee
- = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
+ = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index ccc808ff43e..5e8a2a0f5d8 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -64,7 +64,7 @@
%span.remaining-days= remaining_days
- if !project || can?(current_user, :read_issue, project)
- .block
+ .block.issues
.sidebar-collapsed-icon
%strong
= icon('hashtag', 'aria-hidden': 'true')
@@ -85,11 +85,11 @@
Closed:
= milestone.issues_visible_to_user(current_user).closed.count
- .block
+ .block.merge-requests
.sidebar-collapsed-icon
%strong
= icon('exclamation', 'aria-hidden': 'true')
- %span= milestone.issues_visible_to_user(current_user).count
+ %span= milestone.merge_requests.count
.title.hide-collapsed
Merge requests
%span.badge= milestone.merge_requests.count
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index c0699b13719..aaffc0927eb 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -7,6 +7,7 @@
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true
+- load_pipeline_status(projects)
.js-projects-list-holder
- if projects.any?
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 761f0b606b5..cf0540afb38 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -7,15 +7,17 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project)
+- updated_tooltip = time_ago_with_tooltip(project.updated_at)
%li.project-row{ class: css_class }
= 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
@@ -36,18 +38,21 @@
= markdown_field(project, :description)
.controls
- - if project.archived
- %span.prepend-left-10.label.label-warning archived
- - if project.pipeline_status.has_status?
- %span.prepend-left-10
- = render_project_pipeline_status(project.pipeline_status)
- - if forks
- %span.prepend-left-10
- = icon('code-fork')
- = number_with_delimiter(project.forks_count)
- - if stars
- %span.prepend-left-10
- = icon('star')
- = number_with_delimiter(project.star_count)
- %span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
- = visibility_level_icon(project.visibility_level, fw: true)
+ .prepend-top-0
+ - if project.archived
+ %span.prepend-left-10.label.label-warning archived
+ - if project.pipeline_status.has_status?
+ %span.prepend-left-10
+ = render_project_pipeline_status(project.pipeline_status)
+ - if forks
+ %span.prepend-left-10
+ = icon('code-fork')
+ = number_with_delimiter(project.forks_count)
+ - if stars
+ %span.prepend-left-10
+ = icon('star')
+ = number_with_delimiter(project.star_count)
+ %span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
+ = visibility_level_icon(project.visibility_level, fw: true)
+ .prepend-top-0
+ updated #{updated_tooltip}
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index 74f71e6cbd1..67d186e2874 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,29 +1,24 @@
+- blob = @snippet.blob
.js-file-title.file-title-flex-parent
.file-header-content
- = blob_icon @snippet.mode, @snippet.path
+ = blob_icon blob.mode, blob.path
%strong.file-title-name
- = @snippet.path
+ = blob.path
- = copy_file_path_button(@snippet.path)
+ = copy_file_path_button(blob.path)
+
+ %small
+ = number_to_human_size(blob.raw_size)
.file-actions.hidden-xs
+ = render 'projects/blob/viewer_switcher', blob: blob
+
.btn-group{ role: "group" }<
- = copy_blob_content_button(@snippet)
- = open_raw_file_button(raw_path)
+ = copy_blob_source_button(blob)
+ = open_raw_blob_button(blob)
- if defined?(download_path) && download_path
= link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }
-- if @snippet.content.empty?
- .file-content.code
- .nothing-here-block Empty file
-- else
- - if markup?(@snippet.file_name)
- .file-content.wiki
- - if gitlab_markdown?(@snippet.file_name)
- = preserve(markdown_field(@snippet, :content))
- - else
- = render_markup(@snippet.file_name, @snippet.content)
- - else
- = render 'shared/file_highlight', blob: @snippet
+= render 'projects/blob/content', blob: blob
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index ee3be3c789a..37c3e61912c 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -1,102 +1,82 @@
-.row.prepend-top-default
- .col-lg-3
- %h4.prepend-top-0
- = page_title
- %p
- #{link_to "Webhooks", help_page_path("user/project/integrations/webhooks")} can be
- used for binding events when something is happening within the project.
- .col-lg-9.append-bottom-default
- = form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f|
- = form_errors(hook)
+= form_errors(hook)
- .form-group
- = f.label :url, "URL", class: 'label-light'
- = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json'
- .form-group
- = f.label :token, "Secret Token", class: 'label-light'
- = f.text_field :token, class: "form-control", placeholder: ''
- %p.help-block
- Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
- .form-group
- = f.label :url, "Trigger", class: 'label-light'
- %ul.list-unstyled
- %li
- = f.check_box :push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :push_events, class: 'list-label' do
- %strong Push events
- %p.light
- This URL will be triggered by a push to the repository
- %li
- = f.check_box :tag_push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :tag_push_events, class: 'list-label' do
- %strong Tag push events
- %p.light
- This URL will be triggered when a new tag is pushed to the repository
- %li
- = f.check_box :note_events, class: 'pull-left'
- .prepend-left-20
- = f.label :note_events, class: 'list-label' do
- %strong Comments
- %p.light
- This URL will be triggered when someone adds a comment
- %li
- = f.check_box :issues_events, class: 'pull-left'
- .prepend-left-20
- = f.label :issues_events, class: 'list-label' do
- %strong Issues events
- %p.light
- This URL will be triggered when an issue is created/updated/merged
- %li
- = f.check_box :confidential_issues_events, class: 'pull-left'
- .prepend-left-20
- = f.label :confidential_issues_events, class: 'list-label' do
- %strong Confidential Issues events
- %p.light
- This URL will be triggered when a confidential issue is created/updated/merged
- %li
- = f.check_box :merge_requests_events, class: 'pull-left'
- .prepend-left-20
- = f.label :merge_requests_events, class: 'list-label' do
- %strong Merge Request events
- %p.light
- This URL will be triggered when a merge request is created/updated/merged
- %li
- = f.check_box :build_events, class: 'pull-left'
- .prepend-left-20
- = f.label :build_events, class: 'list-label' do
- %strong Jobs events
- %p.light
- This URL will be triggered when the job status changes
- %li
- = f.check_box :pipeline_events, class: 'pull-left'
- .prepend-left-20
- = f.label :pipeline_events, class: 'list-label' do
- %strong Pipeline events
- %p.light
- This URL will be triggered when the pipeline status changes
- %li
- = f.check_box :wiki_page_events, class: 'pull-left'
- .prepend-left-20
- = f.label :wiki_page_events, class: 'list-label' do
- %strong Wiki Page events
- %p.light
- This URL will be triggered when a wiki page is created/updated
- .form-group
- = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox'
- .checkbox
- = f.label :enable_ssl_verification do
- = f.check_box :enable_ssl_verification
- %strong Enable SSL verification
- = f.submit "Add webhook", class: "btn btn-create"
- %hr
- %h5.prepend-top-default
- Webhooks (#{hooks.count})
- - if hooks.any?
- %ul.well-list
- - hooks.each do |hook|
- = render "project_hook", hook: hook
- - else
- %p.settings-message.text-center.append-bottom-0
- No webhooks found, add one in the form above.
+.form-group
+ = form.label :url, 'URL', class: 'label-light'
+ = form.text_field :url, class: 'form-control', placeholder: 'http://example.com/trigger-ci.json'
+.form-group
+ = form.label :token, 'Secret Token', class: 'label-light'
+ = form.text_field :token, class: 'form-control', placeholder: ''
+ %p.help-block
+ Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
+.form-group
+ = form.label :url, 'Trigger', class: 'label-light'
+ %ul.list-unstyled
+ %li
+ = form.check_box :push_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :push_events, class: 'list-label' do
+ %strong Push events
+ %p.light
+ This URL will be triggered by a push to the repository
+ %li
+ = form.check_box :tag_push_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :tag_push_events, class: 'list-label' do
+ %strong Tag push events
+ %p.light
+ This URL will be triggered when a new tag is pushed to the repository
+ %li
+ = form.check_box :note_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :note_events, class: 'list-label' do
+ %strong Comments
+ %p.light
+ This URL will be triggered when someone adds a comment
+ %li
+ = form.check_box :issues_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :issues_events, class: 'list-label' do
+ %strong Issues events
+ %p.light
+ This URL will be triggered when an issue is created/updated/merged
+ %li
+ = form.check_box :confidential_issues_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :confidential_issues_events, class: 'list-label' do
+ %strong Confidential Issues events
+ %p.light
+ This URL will be triggered when a confidential issue is created/updated/merged
+ %li
+ = form.check_box :merge_requests_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :merge_requests_events, class: 'list-label' do
+ %strong Merge Request events
+ %p.light
+ This URL will be triggered when a merge request is created/updated/merged
+ %li
+ = form.check_box :build_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :build_events, class: 'list-label' do
+ %strong Jobs events
+ %p.light
+ This URL will be triggered when the job status changes
+ %li
+ = form.check_box :pipeline_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :pipeline_events, class: 'list-label' do
+ %strong Pipeline events
+ %p.light
+ This URL will be triggered when the pipeline status changes
+ %li
+ = form.check_box :wiki_page_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :wiki_page_events, class: 'list-label' do
+ %strong Wiki Page events
+ %p.light
+ This URL will be triggered when a wiki page is created/updated
+.form-group
+ = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox'
+ .checkbox
+ = form.label :enable_ssl_verification do
+ = form.check_box :enable_ssl_verification
+ %strong Enable SSL verification
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index e5711ca79c7..8a80013bbfd 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -3,7 +3,7 @@
= render 'shared/snippets/header'
%article.file-holder.snippet-file-content
- = render 'shared/snippets/blob', raw_path: raw_snippet_path(@snippet), download_path: download_snippet_path(@snippet)
+ = render 'shared/snippets/blob', download_path: download_snippet_path(@snippet)
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
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/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/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index eb403c134d1..7b59e976492 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -8,7 +8,7 @@ class ExpireBuildInstanceArtifactsWorker
.reorder(nil)
.find_by(id: build_id)
- return unless build.try(:project)
+ return unless build&.project && !build.project.pending_delete
Rails.logger.info "Removing artifacts for build #{build.id}..."
build.erase_artifacts!
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
new file mode 100644
index 00000000000..603e2f1aaea
--- /dev/null
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -0,0 +1,57 @@
+class ExpirePipelineCacheWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ def perform(pipeline_id)
+ pipeline = Ci::Pipeline.find_by(id: pipeline_id)
+ return unless pipeline
+
+ project = pipeline.project
+ store = Gitlab::EtagCaching::Store.new
+
+ store.touch(project_pipelines_path(project))
+ store.touch(commit_pipelines_path(project, pipeline.commit)) if pipeline.commit
+ store.touch(new_merge_request_pipelines_path(project))
+ each_pipelines_merge_request_path(project, pipeline) do |path|
+ store.touch(path)
+ end
+
+ Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(pipeline)
+ end
+
+ private
+
+ def project_pipelines_path(project)
+ Gitlab::Routing.url_helpers.namespace_project_pipelines_path(
+ project.namespace,
+ project,
+ format: :json)
+ end
+
+ def commit_pipelines_path(project, commit)
+ Gitlab::Routing.url_helpers.pipelines_namespace_project_commit_path(
+ project.namespace,
+ project,
+ commit.id,
+ format: :json)
+ end
+
+ def new_merge_request_pipelines_path(project)
+ Gitlab::Routing.url_helpers.new_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ format: :json)
+ end
+
+ def each_pipelines_merge_request_path(project, pipeline)
+ pipeline.all_merge_requests.each do |merge_request|
+ path = Gitlab::Routing.url_helpers.pipelines_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request,
+ format: :json)
+
+ yield(path)
+ end
+ 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/12910-personal-snippet-prep-2.yml b/changelogs/unreleased/12910-personal-snippet-prep-2.yml
new file mode 100644
index 00000000000..bd9527c30c8
--- /dev/null
+++ b/changelogs/unreleased/12910-personal-snippet-prep-2.yml
@@ -0,0 +1,4 @@
+---
+title: Support Markdown previews for personal snippets
+merge_request: 10810
+author:
diff --git a/changelogs/unreleased/1440-db-backup-ssl-support.yml b/changelogs/unreleased/1440-db-backup-ssl-support.yml
new file mode 100644
index 00000000000..c78bb4fd351
--- /dev/null
+++ b/changelogs/unreleased/1440-db-backup-ssl-support.yml
@@ -0,0 +1,4 @@
+---
+title: Database SSL support for backup script.
+merge_request: 9715
+author: Guillaume Simon
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/19364-webhook-edit.yml b/changelogs/unreleased/19364-webhook-edit.yml
new file mode 100644
index 00000000000..60e154b8b83
--- /dev/null
+++ b/changelogs/unreleased/19364-webhook-edit.yml
@@ -0,0 +1,4 @@
+---
+title: Implement ability to edit hooks
+merge_request: 10816
+author: Alexander Randa
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/20378-natural-sort-issue-numbers.yml b/changelogs/unreleased/20378-natural-sort-issue-numbers.yml
new file mode 100644
index 00000000000..2ebc8485ddf
--- /dev/null
+++ b/changelogs/unreleased/20378-natural-sort-issue-numbers.yml
@@ -0,0 +1,4 @@
+---
+title: Change issues list in MR to natural sorting
+merge_request: 7110
+author: Jeff Stubler
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/2120-issues-search-bar-not-picking-up.yml b/changelogs/unreleased/2120-issues-search-bar-not-picking-up.yml
deleted file mode 100644
index 706609b7baf..00000000000
--- a/changelogs/unreleased/2120-issues-search-bar-not-picking-up.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix filtered search input width for IE
-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/21683-show-created-group-name-flash.yml b/changelogs/unreleased/21683-show-created-group-name-flash.yml
new file mode 100644
index 00000000000..06ef5e972fc
--- /dev/null
+++ b/changelogs/unreleased/21683-show-created-group-name-flash.yml
@@ -0,0 +1,4 @@
+---
+title: Show group name on flash container when group is created from Admin area.
+merge_request: 10905
+author:
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/22826-ui-inconsistency-different-files-views-find-file-button-missing.yml b/changelogs/unreleased/22826-ui-inconsistency-different-files-views-find-file-button-missing.yml
new file mode 100644
index 00000000000..c42fbd4e1f1
--- /dev/null
+++ b/changelogs/unreleased/22826-ui-inconsistency-different-files-views-find-file-button-missing.yml
@@ -0,0 +1,4 @@
+---
+title: Fix UI inconsistency different files view (find file button missing)
+merge_request: 9847
+author: TM Lee
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/26437-closed-by.yml b/changelogs/unreleased/26437-closed-by.yml
new file mode 100644
index 00000000000..6325d3576bc
--- /dev/null
+++ b/changelogs/unreleased/26437-closed-by.yml
@@ -0,0 +1,4 @@
+---
+title: Add issues/:iid/closed_by api endpoint
+merge_request:
+author: mhasbini
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/26488-target-disabled-mr.yml b/changelogs/unreleased/26488-target-disabled-mr.yml
new file mode 100644
index 00000000000..02058481ccf
--- /dev/null
+++ b/changelogs/unreleased/26488-target-disabled-mr.yml
@@ -0,0 +1,4 @@
+---
+title: Disallow merge requests from fork when source project have disabled merge requests
+merge_request:
+author: mhasbini
diff --git a/changelogs/unreleased/26509-show-update-time.yml b/changelogs/unreleased/26509-show-update-time.yml
new file mode 100644
index 00000000000..012fd00dd87
--- /dev/null
+++ b/changelogs/unreleased/26509-show-update-time.yml
@@ -0,0 +1,4 @@
+---
+title: Add update time to project lists.
+merge_request: 8514
+author: Jeff Stubler
diff --git a/changelogs/unreleased/26585-remove-readme-view-caching.yml b/changelogs/unreleased/26585-remove-readme-view-caching.yml
new file mode 100644
index 00000000000..6aefae982bf
--- /dev/null
+++ b/changelogs/unreleased/26585-remove-readme-view-caching.yml
@@ -0,0 +1,4 @@
+---
+title: 'Remove view fragment caching for project READMEs'
+merge_request: 8838
+author:
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/27376-bvl-load-pipelinestatus-in-batch.yml b/changelogs/unreleased/27376-bvl-load-pipelinestatus-in-batch.yml
new file mode 100644
index 00000000000..3d615f5d8a7
--- /dev/null
+++ b/changelogs/unreleased/27376-bvl-load-pipelinestatus-in-batch.yml
@@ -0,0 +1,4 @@
+---
+title: Fetch pipeline status in batch from redis
+merge_request: 10785
+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/27827-cleanup-markdown.yml b/changelogs/unreleased/27827-cleanup-markdown.yml
new file mode 100644
index 00000000000..a8890b78763
--- /dev/null
+++ b/changelogs/unreleased/27827-cleanup-markdown.yml
@@ -0,0 +1,4 @@
+---
+title: Cleanup markdown spacing
+merge_request:
+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/28020-improve-todo-list-when-comes-from-yourself.yml b/changelogs/unreleased/28020-improve-todo-list-when-comes-from-yourself.yml
new file mode 100644
index 00000000000..14aecc35bd2
--- /dev/null
+++ b/changelogs/unreleased/28020-improve-todo-list-when-comes-from-yourself.yml
@@ -0,0 +1,4 @@
+---
+title: Improve text on todo list when the todo action comes from yourself
+merge_request: 10594
+author: Jacopo Beschi @jacopo-beschi
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/28202_decrease_abc_threshold_step1.yml b/changelogs/unreleased/28202_decrease_abc_threshold_step1.yml
new file mode 100644
index 00000000000..8f1520c8b42
--- /dev/null
+++ b/changelogs/unreleased/28202_decrease_abc_threshold_step1.yml
@@ -0,0 +1,4 @@
+---
+title: Decrease ABC threshold to 57.08
+merge_request: 10724
+author: Rydkin Maxim
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/28457-slash-command-board-move.yml b/changelogs/unreleased/28457-slash-command-board-move.yml
new file mode 100644
index 00000000000..cec0f89ed91
--- /dev/null
+++ b/changelogs/unreleased/28457-slash-command-board-move.yml
@@ -0,0 +1,4 @@
+---
+title: Add board_move slash command
+merge_request: 10433
+author: Alex Sanford
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/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/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml b/changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml
new file mode 100644
index 00000000000..6612cfd8866
--- /dev/null
+++ b/changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent people from creating branches if they don't have persmission to push
+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/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/29181-add-more-tests-for-spec-controllers-projects-builds-controller-spec-rb.yml b/changelogs/unreleased/29181-add-more-tests-for-spec-controllers-projects-builds-controller-spec-rb.yml
new file mode 100644
index 00000000000..7a3d687d73f
--- /dev/null
+++ b/changelogs/unreleased/29181-add-more-tests-for-spec-controllers-projects-builds-controller-spec-rb.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve "Add more tests for spec/controllers/projects/builds_controller_spec.rb"
+merge_request: 10244
+author: dosuken123
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/29505-allow-admins-sudo-to-blocked-users.yml b/changelogs/unreleased/29505-allow-admins-sudo-to-blocked-users.yml
new file mode 100644
index 00000000000..42fd71ccd5f
--- /dev/null
+++ b/changelogs/unreleased/29505-allow-admins-sudo-to-blocked-users.yml
@@ -0,0 +1,4 @@
+---
+title: Allow admins to sudo to blocked users via the API
+merge_request: 10842
+author:
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/29712-unnecessary-wait-for-ajax.yml b/changelogs/unreleased/29712-unnecessary-wait-for-ajax.yml
new file mode 100644
index 00000000000..8dc657a4aba
--- /dev/null
+++ b/changelogs/unreleased/29712-unnecessary-wait-for-ajax.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary test helpers includes
+merge_request: 10567
+author: Jacopo Beschi @jacopo-beschi
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/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/29903-remove-user-is-admin-flag-from-api.yml b/changelogs/unreleased/29903-remove-user-is-admin-flag-from-api.yml
new file mode 100644
index 00000000000..a0d497ac1e9
--- /dev/null
+++ b/changelogs/unreleased/29903-remove-user-is-admin-flag-from-api.yml
@@ -0,0 +1,4 @@
+---
+title: Don't display the is_admin flag in most API responses
+merge_request: 10846
+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/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/30305-oauth-token-push-code.yml b/changelogs/unreleased/30305-oauth-token-push-code.yml
new file mode 100644
index 00000000000..aadfb5ca419
--- /dev/null
+++ b/changelogs/unreleased/30305-oauth-token-push-code.yml
@@ -0,0 +1,4 @@
+---
+title: Allow OAuth clients to push code
+merge_request: 10677
+author:
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/30457-expire-note-destroy.yml b/changelogs/unreleased/30457-expire-note-destroy.yml
deleted file mode 100644
index f5c89da68a9..00000000000
--- a/changelogs/unreleased/30457-expire-note-destroy.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix issue's note cache expiration after delete
-merge_request:
-author: mhasbini
diff --git a/changelogs/unreleased/30466-click-x-to-remove-filter.yml b/changelogs/unreleased/30466-click-x-to-remove-filter.yml
new file mode 100644
index 00000000000..2cf08e84ed1
--- /dev/null
+++ b/changelogs/unreleased/30466-click-x-to-remove-filter.yml
@@ -0,0 +1,4 @@
+---
+title: Add button to delete filters from filtered search bar
+merge_request:
+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/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml b/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml
new file mode 100644
index 00000000000..4452b13037b
--- /dev/null
+++ b/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml
@@ -0,0 +1,4 @@
+---
+title: Display GitLab Pages status in Admin Dashboard
+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/30779-show-mr-subnav-issue-tracker.yml b/changelogs/unreleased/30779-show-mr-subnav-issue-tracker.yml
deleted file mode 100644
index 59f8942911c..00000000000
--- a/changelogs/unreleased/30779-show-mr-subnav-issue-tracker.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show sub-nav under Merge Requests when issue tracker is non-default.
-merge_request: 10658
-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/31138-improve-test-settings-for-services-in-empty-projects.yml b/changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml
new file mode 100644
index 00000000000..cb1de425d66
--- /dev/null
+++ b/changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Improves test settings for chat notification services for empty projects
+merge_request: 10886
+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/31254-change-git-commit-command-in-existing-folder.yml b/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml
new file mode 100644
index 00000000000..950336ea932
--- /dev/null
+++ b/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml
@@ -0,0 +1,4 @@
+---
+title: Change Git commit command in Existing folder to git commit -m
+merge_request: 10900
+author: TM Lee
diff --git a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step1.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step1.yml
new file mode 100644
index 00000000000..fedf4de04d3
--- /dev/null
+++ b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step1.yml
@@ -0,0 +1,4 @@
+---
+title: Decrease Cyclomatic Complexity threshold to 16
+merge_request: 10928
+author: Rydkin Maxim
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/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-aria-to-icon.yml b/changelogs/unreleased/add-aria-to-icon.yml
new file mode 100644
index 00000000000..fd6a25784c6
--- /dev/null
+++ b/changelogs/unreleased/add-aria-to-icon.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes an issue preventing screen readers from reading some icons
+merge_request:
+author:
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-tanuki-ci-status-favicons.yml b/changelogs/unreleased/add-tanuki-ci-status-favicons.yml
new file mode 100644
index 00000000000..b60ad81947a
--- /dev/null
+++ b/changelogs/unreleased/add-tanuki-ci-status-favicons.yml
@@ -0,0 +1,4 @@
+---
+title: Updated CI status favicons to include the tanuki
+merge_request: 10923
+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-username-to-activity-feed.yml b/changelogs/unreleased/add-username-to-activity-feed.yml
new file mode 100644
index 00000000000..f4c216a3954
--- /dev/null
+++ b/changelogs/unreleased/add-username-to-activity-feed.yml
@@ -0,0 +1,4 @@
+---
+title: Add username to activity atom feed
+merge_request: 10802
+author: winniehell
diff --git a/changelogs/unreleased/add_index_on_ci_builds_user_id.yml b/changelogs/unreleased/add_index_on_ci_builds_user_id.yml
new file mode 100644
index 00000000000..655ebdb76fa
--- /dev/null
+++ b/changelogs/unreleased/add_index_on_ci_builds_user_id.yml
@@ -0,0 +1,4 @@
+---
+title: Add index on ci_builds.user_id
+merge_request: 10874
+author: blackst0ne
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/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/diff-discussion-buttons-spacing.yml b/changelogs/unreleased/diff-discussion-buttons-spacing.yml
new file mode 100644
index 00000000000..dc76973e55b
--- /dev/null
+++ b/changelogs/unreleased/diff-discussion-buttons-spacing.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed spacing of discussion submit buttons
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-blob-download-button.yml b/changelogs/unreleased/dm-blob-download-button.yml
new file mode 100644
index 00000000000..bd31137b670
--- /dev/null
+++ b/changelogs/unreleased/dm-blob-download-button.yml
@@ -0,0 +1,4 @@
+---
+title: Show Raw button as Download for binary files
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-blob-viewers.yml b/changelogs/unreleased/dm-blob-viewers.yml
new file mode 100644
index 00000000000..5e0d41f3f29
--- /dev/null
+++ b/changelogs/unreleased/dm-blob-viewers.yml
@@ -0,0 +1,5 @@
+---
+title: Add Source/Rendered switch to blobs for SVG, Markdown, Asciidoc and other text
+ files that can be rendered
+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-fix-position-tracer-for-hidden-lines.yml b/changelogs/unreleased/dm-fix-position-tracer-for-hidden-lines.yml
new file mode 100644
index 00000000000..d9ba26a0657
--- /dev/null
+++ b/changelogs/unreleased/dm-fix-position-tracer-for-hidden-lines.yml
@@ -0,0 +1,5 @@
+---
+title: Fix commenting on an existing discussion on an unchanged line that is no longer
+ in the diff
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-sidekiq-5.yml b/changelogs/unreleased/dm-sidekiq-5.yml
new file mode 100644
index 00000000000..69c94b18929
--- /dev/null
+++ b/changelogs/unreleased/dm-sidekiq-5.yml
@@ -0,0 +1,4 @@
+---
+title: Bump Sidekiq to 5.0.0
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-snippet-blob-viewers.yml b/changelogs/unreleased/dm-snippet-blob-viewers.yml
new file mode 100644
index 00000000000..f218095f401
--- /dev/null
+++ b/changelogs/unreleased/dm-snippet-blob-viewers.yml
@@ -0,0 +1,4 @@
+---
+title: Use blob viewers for snippets
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-video-viewer.yml b/changelogs/unreleased/dm-video-viewer.yml
new file mode 100644
index 00000000000..1c42b16e967
--- /dev/null
+++ b/changelogs/unreleased/dm-video-viewer.yml
@@ -0,0 +1,4 @@
+---
+title: Display video blobs in-line like images
+merge_request:
+author:
diff --git a/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml b/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml
new file mode 100644
index 00000000000..a4345b70744
--- /dev/null
+++ b/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml
@@ -0,0 +1,5 @@
+---
+title: Gracefully handle failures for incoming emails which do not match on the To
+ header, and have no References header
+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-fix-project-view.yml b/changelogs/unreleased/dz-fix-project-view.yml
deleted file mode 100644
index 647a1c96bd9..00000000000
--- a/changelogs/unreleased/dz-fix-project-view.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change project view default for existing users and anonymous visitors to files+readme
-merge_request: 10498
-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/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/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-missing-capitalisation-buttons.yml b/changelogs/unreleased/fix-missing-capitalisation-buttons.yml
deleted file mode 100644
index b2c40483475..00000000000
--- a/changelogs/unreleased/fix-missing-capitalisation-buttons.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix missing capitalisation on views
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-notify-post-receive.yml b/changelogs/unreleased/fix-notify-post-receive.yml
new file mode 100644
index 00000000000..6b68396d5c5
--- /dev/null
+++ b/changelogs/unreleased/fix-notify-post-receive.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed wrong method call on notify_post_receive
+merge_request:
+author: Luigi Leoni
diff --git a/changelogs/unreleased/fix-preloading-merge_request_diff.yml b/changelogs/unreleased/fix-preloading-merge_request_diff.yml
deleted file mode 100644
index d38b6b0a707..00000000000
--- a/changelogs/unreleased/fix-preloading-merge_request_diff.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix bad query for PostgreSQL showing merge requests list
-merge_request: 10666
-author:
diff --git a/changelogs/unreleased/fix-project-visibility-setting.yml b/changelogs/unreleased/fix-project-visibility-setting.yml
deleted file mode 100644
index 0fc219ccf52..00000000000
--- a/changelogs/unreleased/fix-project-visibility-setting.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix restricted project visibility setting available to users
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-trace-encoding.yml b/changelogs/unreleased/fix-trace-encoding.yml
deleted file mode 100644
index 152610c43f5..00000000000
--- a/changelogs/unreleased/fix-trace-encoding.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix another case where trace does not have proper encoding set
-merge_request: 10728
-author:
diff --git a/changelogs/unreleased/fix-trace-seeking.yml b/changelogs/unreleased/fix-trace-seeking.yml
deleted file mode 100644
index b753df4bb43..00000000000
--- a/changelogs/unreleased/fix-trace-seeking.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix invalid encoding when showing some traces
-merge_request: 10681
-author:
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-web_hooks-index.yml b/changelogs/unreleased/fix-web_hooks-index.yml
new file mode 100644
index 00000000000..16f233e2e7c
--- /dev/null
+++ b/changelogs/unreleased/fix-web_hooks-index.yml
@@ -0,0 +1,4 @@
+---
+title: Add index to webhooks type column
+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_build_header_line_height.yml b/changelogs/unreleased/fix_build_header_line_height.yml
new file mode 100644
index 00000000000..95b6221f8d2
--- /dev/null
+++ b/changelogs/unreleased/fix_build_header_line_height.yml
@@ -0,0 +1,4 @@
+---
+title: Change line-height on build-header so elements don't overlap
+merge_request:
+author: Dino Maric
diff --git a/changelogs/unreleased/fix_emoji_parser.yml b/changelogs/unreleased/fix_emoji_parser.yml
new file mode 100644
index 00000000000..2b1fffe2457
--- /dev/null
+++ b/changelogs/unreleased/fix_emoji_parser.yml
@@ -0,0 +1,4 @@
+---
+title: Fix rendering emoji inside a string
+merge_request: 10647
+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_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/gl-version-backup-file.yml b/changelogs/unreleased/gl-version-backup-file.yml
new file mode 100644
index 00000000000..9b5abd58ae7
--- /dev/null
+++ b/changelogs/unreleased/gl-version-backup-file.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor backup/restore docs
+merge_request:
+author:
diff --git a/changelogs/unreleased/group-assignee-dropdown-send-group-id.yml b/changelogs/unreleased/group-assignee-dropdown-send-group-id.yml
new file mode 100644
index 00000000000..4f153f9817d
--- /dev/null
+++ b/changelogs/unreleased/group-assignee-dropdown-send-group-id.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed group issues assignee dropdown loading all users
+merge_request:
+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/issues-empty-state-not-centered.yml b/changelogs/unreleased/issues-empty-state-not-centered.yml
deleted file mode 100644
index 883125e28b1..00000000000
--- a/changelogs/unreleased/issues-empty-state-not-centered.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Centered issues empty state
-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_markdown_tables_thinner.yml b/changelogs/unreleased/make_markdown_tables_thinner.yml
new file mode 100644
index 00000000000..d03a26bdeb3
--- /dev/null
+++ b/changelogs/unreleased/make_markdown_tables_thinner.yml
@@ -0,0 +1,4 @@
+---
+title: Make markdown tables thinner
+merge_request: 10909
+author: blackst0ne
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/metrics-graph-error-fix.yml b/changelogs/unreleased/metrics-graph-error-fix.yml
new file mode 100644
index 00000000000..2698b92e1f1
--- /dev/null
+++ b/changelogs/unreleased/metrics-graph-error-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed Prometheus monitoring graphs not showing empty states in certain scenarios
+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/more-mr-filters.yml b/changelogs/unreleased/more-mr-filters.yml
new file mode 100644
index 00000000000..3c2114f6614
--- /dev/null
+++ b/changelogs/unreleased/more-mr-filters.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Filter merge requests by milestone and labels'
+merge_request: Robert Schilling
+author: 10924
diff --git a/changelogs/unreleased/mr-diff-size-overflow.yml b/changelogs/unreleased/mr-diff-size-overflow.yml
new file mode 100644
index 00000000000..87449930cf2
--- /dev/null
+++ b/changelogs/unreleased/mr-diff-size-overflow.yml
@@ -0,0 +1,4 @@
+---
+title: Show sizes correctly in merge requests when diffs overflow
+merge_request:
+author:
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/mr-widget-bug-fix.yml b/changelogs/unreleased/mr-widget-bug-fix.yml
deleted file mode 100644
index 9af29d3927e..00000000000
--- a/changelogs/unreleased/mr-widget-bug-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix MR widget bug that merged a MR when Merge when pipeline succeeds was clicked
- via the dropdown
-merge_request: 10611
-author:
diff --git a/changelogs/unreleased/mrchrisw-22740-merge-api.yml b/changelogs/unreleased/mrchrisw-22740-merge-api.yml
new file mode 100644
index 00000000000..e75160aec70
--- /dev/null
+++ b/changelogs/unreleased/mrchrisw-22740-merge-api.yml
@@ -0,0 +1,4 @@
+---
+title: Fix updating merge_when_build_succeeds via merge API endpoint
+merge_request: 10873
+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/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/pms-lighter-colors.yml b/changelogs/unreleased/pms-lighter-colors.yml
deleted file mode 100644
index 958d4bc0ac0..00000000000
--- a/changelogs/unreleased/pms-lighter-colors.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add lighter colors and fix existing light colors
-merge_request: 10690
-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/related-branch-ci-status-icon-alignment.yml b/changelogs/unreleased/related-branch-ci-status-icon-alignment.yml
new file mode 100644
index 00000000000..198b6ce15ae
--- /dev/null
+++ b/changelogs/unreleased/related-branch-ci-status-icon-alignment.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed alignment of CI icon in issues related branches
+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_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/replace_header_mr_icon.yml b/changelogs/unreleased/replace_header_mr_icon.yml
new file mode 100644
index 00000000000..2ef6500f88a
--- /dev/null
+++ b/changelogs/unreleased/replace_header_mr_icon.yml
@@ -0,0 +1,4 @@
+---
+title: Replace header merge request icon
+merge_request: 10932
+author: blackst0ne
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-bump-sidekiq-version.yml b/changelogs/unreleased/sh-bump-sidekiq-version.yml
new file mode 100644
index 00000000000..5369b78b76a
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-sidekiq-version.yml
@@ -0,0 +1,4 @@
+---
+title: Upgrade Sidekiq to 4.2.10
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-optimize-duplicate-routable-full-path.yml b/changelogs/unreleased/sh-optimize-duplicate-routable-full-path.yml
new file mode 100644
index 00000000000..b1ef00f09b2
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-duplicate-routable-full-path.yml
@@ -0,0 +1,4 @@
+---
+title: Cache Routable#full_path in RequestStore to reduce duplicate route loads
+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/siemens-gitlab-ce-fix-subgroup-hide-button.yml b/changelogs/unreleased/siemens-gitlab-ce-fix-subgroup-hide-button.yml
deleted file mode 100644
index 716311c7582..00000000000
--- a/changelogs/unreleased/siemens-gitlab-ce-fix-subgroup-hide-button.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Hide new subgroup button if user has no permission to create one
-merge_request: 10627
-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/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-make-user-master-project-by-admin.yml b/changelogs/unreleased/tc-make-user-master-project-by-admin.yml
new file mode 100644
index 00000000000..459d6178bdd
--- /dev/null
+++ b/changelogs/unreleased/tc-make-user-master-project-by-admin.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure namespace owner is Master of project upon creation
+merge_request: 10910
+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/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-api-fix-build-events.yml b/changelogs/unreleased/zj-api-fix-build-events.yml
deleted file mode 100644
index 7700d8dcd22..00000000000
--- a/changelogs/unreleased/zj-api-fix-build-events.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Bugfix: POST /projects/:id/hooks and PUT /projects/:id/hook/:hook_id no longer ignore the the job_events param in the V4 API"
-merge_request: 10586
-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-dockerfiles.yml b/changelogs/unreleased/zj-dockerfiles.yml
new file mode 100644
index 00000000000..40cb7dcfb76
--- /dev/null
+++ b/changelogs/unreleased/zj-dockerfiles.yml
@@ -0,0 +1,4 @@
+---
+title: Dockerfiles templates are imported from gitlab.com/gitlab-org/Dockerfile
+merge_request: 10663
+author:
diff --git a/changelogs/unreleased/zj-fk-ci-triggers.yml b/changelogs/unreleased/zj-fk-ci-triggers.yml
deleted file mode 100644
index 9fe708b25c0..00000000000
--- a/changelogs/unreleased/zj-fk-ci-triggers.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add foreign key for ci_trigger_requests on ci_triggers
-merge_request: 10537
-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/database.yml.mysql b/config/database.yml.mysql
index a33e40e8eb3..db1b712d3bc 100644
--- a/config/database.yml.mysql
+++ b/config/database.yml.mysql
@@ -25,6 +25,7 @@ development:
pool: 5
username: root
password: "secure password"
+ # host: localhost
# socket: /tmp/mysql.sock
# Warning: The database defined as "test" will be erased and
@@ -39,4 +40,5 @@ test: &test
pool: 5
username: root
password:
+ # host: localhost
# socket: /tmp/mysql.sock
diff --git a/config/database.yml.postgresql b/config/database.yml.postgresql
index 7067e0fe402..c517a4c0cb8 100644
--- a/config/database.yml.postgresql
+++ b/config/database.yml.postgresql
@@ -9,7 +9,7 @@ production:
# username: git
# password:
# host: localhost
- # port: 5432
+ # port: 5432
#
# Development specific
@@ -21,6 +21,7 @@ development:
pool: 5
username: postgres
password:
+ # host: localhost
#
# Staging specific
@@ -32,6 +33,7 @@ staging:
pool: 5
username: postgres
password:
+ # host: localhost
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
@@ -43,3 +45,4 @@ test: &test
pool: 5
username: postgres
password:
+ # host: localhost
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 06c9f734c2a..c2eaf263937 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -505,6 +505,11 @@ production: &base
# If you use non-standard ssh port you need to specify it
# ssh_port: 22
+ workhorse:
+ # File that contains the secret key for verifying access for gitlab-workhorse.
+ # Default is '.gitlab_workhorse_secret' relative to Rails.root (i.e. root of the GitLab app).
+ # secret_file: /home/git/gitlab/.gitlab_workhorse_secret
+
## Git settings
# CAUTION!
# Use the default values unless you really know what you are doing
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4c9d829aa9f..7a8f00f11b2 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,13 @@ 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)
+
+#
+# Workhorse
+#
+Settings['workhorse'] ||= Settingslogic.new({})
+Settings.workhorse['secret_file'] ||= Rails.root.join('.gitlab_workhorse_secret')
#
# Repositories
diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb
index 1933afcbfb1..cd7df44351a 100644
--- a/config/initializers/carrierwave.rb
+++ b/config/initializers/carrierwave.rb
@@ -6,6 +6,8 @@ if File.exist?(aws_file)
AWS_CONFIG = YAML.load(File.read(aws_file))[Rails.env]
CarrierWave.configure do |config|
+ config.fog_provider = 'fog/aws'
+
config.fog_credentials = {
provider: 'AWS', # required
aws_access_key_id: AWS_CONFIG['access_key_id'], # required
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
index 764c067c6f0..a7efd74f09e 100644
--- a/config/initializers/rspec_profiling.rb
+++ b/config/initializers/rspec_profiling.rb
@@ -36,10 +36,10 @@ if Rails.env.test?
RspecProfiling::Collectors::PSQL.prepend(RspecProfilingExt::PSQL)
config.collector = RspecProfiling::Collectors::PSQL
end
- end
- if ENV.has_key?('CI')
- RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git)
- RspecProfiling::Run.prepend(RspecProfilingExt::Run)
+ if ENV.key?('CI')
+ RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git)
+ RspecProfiling::Run.prepend(RspecProfilingExt::Run)
+ end
end
end
diff --git a/config/routes.rb b/config/routes.rb
index 1da226a3b57..2584981bb04 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -99,5 +99,7 @@ Rails.application.routes.draw do
end
end
+ draw :test if Rails.env.test?
+
get '*unmatched_route', to: 'application#route_not_found'
end
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 486ce3c5c87..48993420ed9 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -50,8 +50,10 @@ namespace :admin do
resources :deploy_keys, only: [:index, :new, :create, :destroy]
- resources :hooks, only: [:index, :create, :destroy] do
- get :test
+ resources :hooks, only: [:index, :create, :edit, :update, :destroy] do
+ member do
+ get :test
+ end
end
resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
@@ -91,6 +93,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 +108,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 fa92202c1ea..71fc84a7a12 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -173,7 +173,7 @@ constraints(ProjectUrlConstrainer.new) do
post :retry
post :play
post :erase
- get :trace
+ get :trace, defaults: { format: 'json' }
get :raw
end
@@ -185,7 +185,7 @@ constraints(ProjectUrlConstrainer.new) do
end
end
- resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
+ resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do
member do
get :test
end
diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb
index ce0d1314292..56534f677be 100644
--- a/config/routes/snippets.rb
+++ b/config/routes/snippets.rb
@@ -3,6 +3,7 @@ resources :snippets, concerns: :awardable do
get 'raw'
get 'download'
post :mark_as_spam
+ post :preview_markdown
end
end
diff --git a/config/routes/test.rb b/config/routes/test.rb
new file mode 100644
index 00000000000..ac477cdbbbc
--- /dev/null
+++ b/config/routes/test.rb
@@ -0,0 +1,2 @@
+get '/unicorn_test/pid' => 'unicorn_test#pid'
+post '/unicorn_test/kill' => 'unicorn_test#kill'
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 64a04dc342e..0ec9e48845e 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -19,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',
@@ -32,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: {
@@ -78,6 +78,11 @@ var config = {
loader: 'raw-loader',
},
{
+ test: /\.gif$/,
+ loader: 'url-loader',
+ query: { mimetype: 'image/gif' },
+ },
+ {
test: /\.(worker\.js|pdf)$/,
exclude: /node_modules/,
loader: 'file-loader',
@@ -121,11 +126,11 @@ var config = {
'environments',
'environments_folder',
'issuable',
+ 'issue_show',
'merge_conflicts',
'notebook_viewer',
'pdf_viewer',
- 'vue_pipelines',
- 'issue_show',
+ 'pipelines',
],
minChunks: function(module, count) {
return module.resource && (/vue_shared/).test(module.resource);
diff --git a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
index 69d64ccd006..22bac46e25c 100644
--- a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
+++ b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
class AddOnlyAllowMergeIfBuildSucceedsToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160608195742_add_repository_storage_to_projects.rb b/db/migrate/20160608195742_add_repository_storage_to_projects.rb
index c700d2b569d..0f3664c13ef 100644
--- a/db/migrate/20160608195742_add_repository_storage_to_projects.rb
+++ b/db/migrate/20160608195742_add_repository_storage_to_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
class AddRepositoryStorageToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
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/20160715154212_add_request_access_enabled_to_projects.rb b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb
index bf0131c6d76..5dc26f8982a 100644
--- a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb
+++ b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
class AddRequestAccessEnabledToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb
index e7b14cd3ee2..4a317646788 100644
--- a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb
+++ b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
class AddRequestAccessEnabledToGroups < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
index a2c207b49ea..7414a28ac97 100644
--- a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
+++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
class RemoveFeaturesEnabledFromProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
index 18ea9d43a43..0100e30a733 100644
--- a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
+++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
class RemoveProjectsPushesSinceGc < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
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/20170124193147_add_two_factor_columns_to_namespaces.rb b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb
index df5cddeb205..ae37da275fd 100644
--- a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb
+++ b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
class AddTwoFactorColumnsToNamespaces < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb
index 1d1021fcbb3..8d4aefa4365 100644
--- a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb
+++ b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
class AddTwoFactorColumnsToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
index f54608ecceb..7ad01a04815 100644
--- a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
+++ b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
class AddPrintingMergeRequestLinkEnabledToProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb
index aa64f2dddca..f335e77fb5e 100644
--- a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb
+++ b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
class AddAutoCancelPendingPipelinesToProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
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/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
index b39c0a3be0f..6c9fe19ca34 100644
--- a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
+++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
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/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/migrate/20170421102337_remove_nil_type_services.rb b/db/migrate/20170421102337_remove_nil_type_services.rb
new file mode 100644
index 00000000000..b835b9c6ed9
--- /dev/null
+++ b/db/migrate/20170421102337_remove_nil_type_services.rb
@@ -0,0 +1,12 @@
+class RemoveNilTypeServices < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute <<-SQL
+ DELETE FROM services WHERE type IS NULL OR type = '';
+ SQL
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20170423064036_add_index_on_ci_builds_updated_at.rb b/db/migrate/20170423064036_add_index_on_ci_builds_updated_at.rb
new file mode 100644
index 00000000000..0bbb74ee05e
--- /dev/null
+++ b/db/migrate/20170423064036_add_index_on_ci_builds_updated_at.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 AddIndexOnCiBuildsUpdatedAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds, :updated_at
+ end
+
+ def down
+ remove_concurrent_index :ci_builds, :updated_at if index_exists?(:ci_builds, :updated_at)
+ end
+end
diff --git a/db/migrate/20170424095707_add_index_on_ci_builds_user_id.rb b/db/migrate/20170424095707_add_index_on_ci_builds_user_id.rb
new file mode 100644
index 00000000000..348d5dbc270
--- /dev/null
+++ b/db/migrate/20170424095707_add_index_on_ci_builds_user_id.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 AddIndexOnCiBuildsUserId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds, :user_id
+ end
+
+ def down
+ remove_concurrent_index :ci_builds, :user_id if index_exists?(:ci_builds, :user_id)
+ end
+end
diff --git a/db/migrate/20170424142900_add_index_to_web_hooks_type.rb b/db/migrate/20170424142900_add_index_to_web_hooks_type.rb
new file mode 100644
index 00000000000..9af158e3844
--- /dev/null
+++ b/db/migrate/20170424142900_add_index_to_web_hooks_type.rb
@@ -0,0 +1,15 @@
+class AddIndexToWebHooksType < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :web_hooks, :type
+ end
+
+ def down
+ remove_concurrent_index :web_hooks, :type
+ end
+end
diff --git a/db/migrate/20170426175636_fill_missing_uuid_on_application_settings.rb b/db/migrate/20170426175636_fill_missing_uuid_on_application_settings.rb
new file mode 100644
index 00000000000..58ad2c64075
--- /dev/null
+++ b/db/migrate/20170426175636_fill_missing_uuid_on_application_settings.rb
@@ -0,0 +1,10 @@
+class FillMissingUuidOnApplicationSettings < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute("UPDATE application_settings SET uuid = #{quote(SecureRandom.uuid)} WHERE uuid is NULL")
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20170426181740_add_index_on_ci_runners_contacted_at.rb b/db/migrate/20170426181740_add_index_on_ci_runners_contacted_at.rb
new file mode 100644
index 00000000000..879825a1934
--- /dev/null
+++ b/db/migrate/20170426181740_add_index_on_ci_runners_contacted_at.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 AddIndexOnCiRunnersContactedAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_runners, :contacted_at
+ end
+
+ def down
+ remove_concurrent_index :ci_runners, :contacted_at if index_exists?(:ci_runners, :contacted_at)
+ 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/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/schema.rb b/db/schema.rb
index 5689f7331dc..b938657a186 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170408033905) do
+ActiveRecord::Schema.define(version: 20170426181740) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -24,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|
@@ -34,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|
@@ -116,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|
@@ -159,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|
@@ -235,6 +241,8 @@ ActiveRecord::Schema.define(version: 20170408033905) do
add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
+ add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
+ add_index "ci_builds", ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
create_table "ci_pipelines", force: :cascade do |t|
t.string "ref"
@@ -288,6 +296,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
t.boolean "locked", default: false, null: false
end
+ add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
add_index "ci_runners", ["is_shared"], name: "index_ci_runners_on_is_shared", using: :btree
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
@@ -477,6 +486,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
@@ -541,6 +551,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
@@ -661,6 +672,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
@@ -698,6 +710,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"}
@@ -724,6 +737,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
@@ -758,6 +772,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
@@ -954,6 +969,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
@@ -1026,6 +1042,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
@@ -1097,6 +1114,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
@@ -1140,6 +1158,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"
@@ -1301,6 +1321,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
@@ -1352,6 +1373,7 @@ ActiveRecord::Schema.define(version: 20170408033905) do
end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
+ add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree
add_foreign_key "boards", "projects"
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index b6790b4d008..6406040da4b 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,82 +1,195 @@
# GitLab Community Edition
-All technical content published by GitLab lives in the documentation, including:
-
-- **General Documentation**
- - [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.
-
-## User documentation
-
-- [Account Security](user/profile/account/two_factor_authentication.md) Securing your account via two-factor authentication, etc.
-- [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.
-- [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.
-- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
-- [GitLab Pages](user/project/pages/index.md) Using GitLab Pages.
+[GitLab](https://about.gitlab.com/) is a Git-based fully featured platform
+for software development.
+
+**GitLab Community Edition (CE)** is an opensource product, self-hosted, free to use.
+All [GitLab products](https://about.gitlab.com/products/) contain the features
+available in GitLab CE. Premium features are available in
+[GitLab Enterprise Edition (EE)](https://about.gitlab.com/gitlab-ee/).
+
+----
+
+Shortcuts to GitLab's most visited docs:
+
+| [GitLab CI](ci/README.md) | Other |
+| :----- | :----- |
+| [Quick start guide](ci/quick_start/README.md) | [API](api/README.md) |
+| [Configuring `.gitlab-ci.yml`](ci/yaml/README.md) | [SSH authentication](ssh/README.md) |
+| [Using Docker images](ci/docker/using_docker_images.md) | [GitLab Pages](user/project/pages/index.md) |
+
+## Getting started with GitLab
+
+- [GitLab Basics](gitlab-basics/README.md): Start working on your command line and on GitLab.
+- [GitLab Workflow](workflow/README.md): Enhance your workflow with the best of GitLab Workflow.
+ - See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
+- [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown).
+- [GitLab Slash Commands](user/project/slash_commands.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
+
+### User account
+
+- [Authentication](topics/authentication/index.md): Account security with two-factor authentication, setup your ssh keys and deploy keys for secure access to your projects.
+- [Profile settings](profile/README.md): Manage your profile settings, two factor authentication and more.
+- [User permissions](user/permissions.md): Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
+
+### Projects and groups
+
+- [Create a project](gitlab-basics/create-project.md)
+- [Fork a project](gitlab-basics/fork-project.md)
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
-- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
-- [Markdown](user/markdown.md) GitLab's advanced formatting system.
-- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
-- [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
-- [Profile Settings](profile/README.md)
-- [Project Services](user/project/integrations/project_services.md) Integrate a project with external services, such as CI and chat.
-- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
+- [Project access](public_access/public_access.md): Setting up your project's visibility to public, internal, or private.
+- [Groups](workflow/groups.md): Organize your projects in groups.
+ - [Create a group](gitlab-basics/create-group.md)
+ - [GitLab Subgroups](user/group/subgroups/index.md)
- [Search through GitLab](user/search/index.md): Search for issues, merge requests, projects, groups, todos, and issues in Issue Boards.
-- [Snippets](user/snippets.md) Snippets allow you to create little bits of code.
-- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
-- [Webhooks](user/project/integrations/webhooks.md) Let GitLab notify you when new code has been pushed to your project.
-- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
+
+### Repository
+
+Manage files and branches from the UI (user interface):
+
+- Files
+ - [Create a file](user/project/repository/web_editor.md#create-a-file)
+ - [Upload a file](user/project/repository/web_editor.md#upload-a-file)
+ - [File templates](user/project/repository/web_editor.md#template-dropdowns)
+ - [Create a directory](user/project/repository/web_editor.md#create-a-directory)
+ - [Start a merge request](user/project/repository/web_editor.md#tips) (when committing via UI)
+- Branches
+ - [Create a branch](user/project/repository/web_editor.md#create-a-new-branch)
+ - [Protected branches](user/project/protected_branches.md#protected-branches)
+
+### Issues and Merge Requests (MRs)
+
+- [Discussions](user/discussions/index.md) Threads, comments, and resolvable discussions in issues, commits, and merge requests.
+- Issues
+ - [Create an issue](gitlab-basics/create-issue.md#how-to-create-an-issue-in-gitlab)
+ - [Confidential Issues](user/project/issues/confidential_issues.md)
+ - [Automatic issue closing](user/project/issues/automatic_issue_closing.md)
+ - [Issue Boards](user/project/issue_board.md)
+- [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests.
+- [Labels](user/project/labels.md): Categorize your issues or merge requests based on descriptive titles.
+- [Merge Requests](user/project/merge_requests/index.md)
+ - [Work In Progress Merge Requests](user/project/merge_requests/work_in_progress_merge_requests.md)
+ - [Merge Request discussion resolution](user/discussions/index.md#moving-a-single-discussion-to-a-new-issue): Resolve discussions, move discussions in a merge request to an issue, only allow merge requests to be merged if all discussions are resolved.
+ - [Checkout merge requests locally](user/project/merge_requests/index.md#checkout-merge-requests-locally)
+ - [Cherry-pick](user/project/merge_requests/cherry_pick_changes.md)
+- [Milestones](user/project/milestones/index.md): Organize issues and merge requests into a cohesive group, optionally setting a due date.
+- [Todos](workflow/todos.md): A chronological list of to-dos that are waiting for your input, all in a simple dashboard.
+
+### Git and GitLab
+
+- [Git](topics/git/index.md): Getting started with Git, branching strategies, Git LFS, advanced use.
+- [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 Flow](workflow/gitlab_flow.md): explore the best of Git with the GitLab Flow strategy.
+
+### Migrate and import your projects from other platforms
+
+- [Importing to GitLab](workflow/importing/README.md): Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
+- [Migrating from SVN](workflow/importing/migrating_from_svn.md): Convert a SVN repository to Git and GitLab.
+
+## GitLab's superpowers
+
+Take a step ahead and dive into GitLab's advanced features.
+
+- [GitLab Pages](user/project/pages/index.md): Build, test, and deploy your static website with GitLab Pages.
+- [Snippets](user/snippets.md): Snippets allow you to create little bits of code.
+- [Wikis](workflow/project_features.md#wiki): Enhance your repository documentation with built-in wikis.
+
+### Continuous Integration, Delivery, and Deployment
+
+- [GitLab CI](ci/README.md): Explore the features and capabilities of Continuous Integration, Continuous Delivery, and Continuous Deployment with GitLab.
+ - [Auto Deploy](ci/autodeploy/index.md): Configure GitLab CI for the deployment of your application.
+ - [Review Apps](ci/review_apps/index.md): Preview changes to your app right from a merge request.
+- [GitLab Cycle Analytics](user/project/cycle_analytics.md): Cycle Analytics measures the time it takes to go from an [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab) for each project you have.
+- [GitLab Container Registry](user/project/container_registry.md): Learn how to use GitLab's built-in Container Registry.
+
+### Automation
+
+- [API](api/README.md): Automate GitLab via a simple and powerful API.
+- [GitLab Webhooks](user/project/integrations/webhooks.md): Let GitLab notify you when new code has been pushed to your project.
+
+### Integrations
+
+- [Project Services](user/project/integrations/project_services.md): Integrate a project with external services, such as CI and chat.
+- [GitLab Integration](integration/README.md): Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication.
+
+----
## Administrator documentation
-- [Access restrictions](user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols) Define which Git access protocols can be used to talk to GitLab
-- [Authentication/Authorization](administration/auth/README.md) Configure external authentication with LDAP, SAML, CAS and additional Omniauth providers.
-- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab.
-- [Custom Git hooks](administration/custom_hooks.md) Custom Git hooks (on the filesystem) for when webhooks aren't enough.
-- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
-- [Environment Variables](administration/environment_variables.md) to configure GitLab.
-- [Git LFS configuration](workflow/lfs/lfs_administration.md)
-- [GitLab Pages configuration](administration/pages/index.md) Configure GitLab Pages.
-- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
-- [GitLab performance monitoring with Prometheus](administration/monitoring/prometheus/index.md) Configure GitLab and Prometheus for measuring performance metrics.
-- [Header logo](customization/branded_page_and_email_header.md) Change the logo on the overall page and email header.
-- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability.
-- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
-- [Install](install/README.md) Requirements, directory structures and installation from source.
-- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
-- [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages.
-- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab.
-- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars.
-- [Log system](administration/logs.md) Log system.
-- [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.
-- [Repository storage paths](administration/repository_storage_paths.md) Manage the paths used to store repositories.
-- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests.
-- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components.
-- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
-- [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.
-- [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.
+Learn how to administer your GitLab instance. Regular users don't
+have access to GitLab administration tools and settings.
+
+### Install, update, upgrade, migrate
+
+- [Install](install/README.md): Requirements, directory structures and installation from source.
+- [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/): Integrate [Mattermost](https://about.mattermost.com/) with your GitLab installation.
+- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md): If you have an old GitLab installation (older than 8.0), follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
+- [Restart GitLab](administration/restart_gitlab.md): Learn how to restart GitLab and its components.
+- [Update](update/README.md): Update guides to upgrade your installation.
+
+### User permissions
+
+- [Access restrictions](user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab
+- [Authentication/Authorization](topics/authentication/index.md#gitlab-administrators): Enforce 2FA, configure external authentication with LDAP, SAML, CAS and additional Omniauth providers.
+
+### GitLab admins' superpowers
+
+- [Container Registry](administration/container_registry.md): Configure Docker Registry with GitLab.
+- [Custom Git hooks](administration/custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough.
+- [Git LFS configuration](workflow/lfs/lfs_administration.md): Learn how to use LFS under GitLab.
+- [GitLab Pages configuration](administration/pages/index.md): Configure GitLab Pages.
+- [High Availability](administration/high_availability/README.md): Configure multiple servers for scaling or high availability.
+- [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.
+- GitLab CI
+ - [CI admin settings](user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration time.
+
+### Integrations
+
+- [Integrations](integration/README.md): How to integrate with systems such as JIRA, Redmine, Twitter.
+- [Koding](administration/integration/koding.md): Set up Koding to use with GitLab.
+- [Mattermost](user/project/integrations/mattermost.md): Set up GitLab with Mattermost.
+
+### Monitoring
+
+- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md): Configure GitLab and InfluxDB for measuring performance metrics.
+- [GitLab performance monitoring with Prometheus](administration/monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics.
+- [Monitoring uptime](user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint.
+
+### Performance
+
+- [Housekeeping](administration/housekeeping.md): Keep your Git repository tidy and fast.
+- [Operations](administration/operations.md): Keeping GitLab up and running.
+- [Polling](administration/polling.md): Configure how often the GitLab UI polls for updates.
+- [Request Profiling](administration/monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
+
+### Customization
+
+- [Adjust your instance's timezone](workflow/timezone.md): Customize the default time zone of GitLab.
+- [Environment variables](administration/environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab.
+- [Header logo](customization/branded_page_and_email_header.md): Change the logo on the overall page and email header.
+- [Issue closing pattern](administration/issue_closing_pattern.md): Customize how to close an issue from commit messages.
+- [Libravatar](customization/libravatar.md): Use Libravatar instead of Gravatar for user avatars.
+- [Welcome message](customization/welcome_message.md): Add a custom welcome message to the sign-in page.
+
+### Admin tools
+
+- [Raketasks](raketasks/README.md): Backups, maintenance, automatic webhook setup and the importing of projects.
+ - [Backup and restore](raketasks/backup_restore.md): Backup and restore your GitLab instance.
+- [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.
+- [Repository storage paths](administration/repository_storage_paths.md): Manage the paths used to store repositories.
+- [Security](security/README.md): Learn what you can do to further secure your GitLab instance.
+- [System hooks](system_hooks/system_hooks.md): Notifications when users, projects and keys are changed.
+
+### Troubleshooting
+
+- [Debugging tips](administration/troubleshooting/debug.md): Tips to debug problems when things go wrong
+- [Log system](administration/logs.md): Where to look for logs.
+- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md): Debug when Sidekiq appears hung and is not processing jobs.
## Contributor documentation
-- [Development](development/README.md) All styleguides and explanations how to contribute.
-- [Legal](legal/README.md) Contributor license agreements.
+- [Development](development/README.md): All styleguides and explanations how to contribute.
+- [Legal](legal/README.md): Contributor license agreements.
+- [Writing documentation](development/writing_documentation.md): Contributing to GitLab Docs.
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index f6027b2f99e..725fc1f6076 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -65,14 +65,14 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
#
# Example: 'Paris' or 'Acme, Ltd.'
label: 'LDAP'
-
+
# Example: 'ldap.mydomain.com'
host: '_your_ldap_server'
# This port is an example, it is sometimes different but it is always an integer and not a string
port: 389
- uid: 'sAMAccountName'
+ uid: 'sAMAccountName' # This should be the attribute, not the value that maps to uid.
method: 'plain' # "tls" or "ssl" or "plain"
-
+
# Examples: 'america\\momo' or 'CN=Gitlab Git,CN=Users,DC=mydomain,DC=com'
bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
password: '_the_password_of_the_bind_user'
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 2e22212ddde..6c6942a7bfe 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -1,6 +1,6 @@
# Gitaly
-[Gitaly](https://gitlab.com/gitlab-org/gitlay) (introduced in GitLab
+[Gitaly](https://gitlab.com/gitlab-org/gitaly) (introduced in GitLab
9.0) is a service that provides high-level RPC access to Git
repositories. As of GitLab 9.1 it is still an optional component with
limited scope.
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index 3245988fc14..d9ca74ca1a3 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -18,7 +18,8 @@ you need to use with GitLab.
## GitLab Pages Ports
-If you're using GitLab Pages you will need some additional port configurations.
+If you're using GitLab Pages with custom domain support you will need some
+additional port configurations.
GitLab Pages requires a separate virtual IP address. Configure DNS to point the
`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the
[GitLab Pages documentation][gitlab-pages] for more information.
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index bf1aa6b9ac5..c5125dc6d5a 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -7,21 +7,20 @@ supported natively in NFS version 4. NFSv3 also supports locking as long as
Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not
specifically test NFSv3.
-**no_root_squash**: NFS normally changes the `root` user to `nobody`. This is
-a good security measure when NFS shares will be accessed by many different
-users. However, in this case only GitLab will use the NFS share so it
-is safe. GitLab requires the `no_root_squash` setting because we need to
-manage file permissions automatically. Without the setting you will receive
-errors when the Omnibus package tries to alter permissions. Note that GitLab
-and other bundled components do **not** run as `root` but as non-privileged
-users. The requirement for `no_root_squash` is to allow the Omnibus package to
-set ownership and permissions on files, as needed.
-
### Recommended options
When you define your NFS exports, we recommend you also add the following
options:
+- `no_root_squash` - NFS normally changes the `root` user to `nobody`. This is
+ a good security measure when NFS shares will be accessed by many different
+ users. However, in this case only GitLab will use the NFS share so it
+ is safe. GitLab recommends the `no_root_squash` setting because we need to
+ manage file permissions automatically. Without the setting you may receive
+ errors when the Omnibus package tries to alter permissions. Note that GitLab
+ and other bundled components do **not** run as `root` but as non-privileged
+ users. The recommendation for `no_root_squash` is to allow the Omnibus package
+ to set ownership and permissions on files, as needed.
- `sync` - Force synchronous behavior. Default is asynchronous and under certain
circumstances it could lead to data loss if a failure occurs before data has
synced.
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/integration/terminal.md b/doc/administration/integration/terminal.md
index 3b5ee86b68b..91e844c7b42 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -32,7 +32,7 @@ In brief:
As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of
Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers
-through to the next one in the chain. If you installed Gitlab using Omnibus, or
+through to the next one in the chain. If you installed GitLab using Omnibus, or
from source, starting with GitLab 8.15, this should be done by the default
configuration, so there's no need for you to do anything.
@@ -58,7 +58,7 @@ document for more details.
If you'd like to disable web terminal support in GitLab, just stop passing
the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse
proxy in the chain. For most users, this will be the NGINX server bundled with
-Omnibus Gitlab, in which case, you need to:
+Omnibus GitLab, in which case, you need to:
* Find the `nginx['proxy_set_headers']` section of your `gitlab.rb` file
* Ensure the whole block is uncommented, and then comment out or remove the
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index 0273c819614..ab9e63e01d3 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -48,7 +48,6 @@ Example of response
"bio": null,
"created_at": "2016-08-11T07:09:20.351Z",
"id": 1,
- "is_admin": true,
"linkedin": "",
"location": null,
"name": "Administrator",
@@ -106,7 +105,6 @@ Example of response
"bio": null,
"created_at": "2016-08-11T07:09:20.351Z",
"id": 1,
- "is_admin": true,
"linkedin": "",
"location": null,
"name": "Administrator",
@@ -195,7 +193,6 @@ Example of response
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root",
"created_at": "2016-08-11T07:09:20.351Z",
- "is_admin": true,
"bio": null,
"location": null,
"skype": "",
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 5f01fcdd396..6c10b5ab0e7 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -823,6 +823,67 @@ Example response:
}
```
+## List merge requests that will close issue on merge
+
+Get all the merge requests that will close issue when merged.
+
+```
+GET /projects/:id/issues/:issue_iid/closed_by
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project issue |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/11/closed_by
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 6471,
+ "iid": 6432,
+ "project_id": 1,
+ "title": "add a test for cgi lexer options",
+ "description": "closes #11",
+ "state": "opened",
+ "created_at": "2017-04-06T18:33:34.168Z",
+ "updated_at": "2017-04-09T20:10:24.983Z",
+ "target_branch": "master",
+ "source_branch": "feature.custom-highlighting",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/root"
+ },
+ "assignee": null,
+ "source_project_id": 1,
+ "target_project_id": 1,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": null,
+ "merge_when_pipeline_succeeds": false,
+ "merge_status": "unchecked",
+ "sha": "5a62481d563af92b8e32d735f2fa63b94e806835",
+ "merge_commit_sha": null,
+ "user_notes_count": 1,
+ "should_remove_source_branch": null,
+ "force_remove_source_branch": false,
+ "web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/merge_requests/6432"
+ }
+]
+```
+
+
## Comments on issues
Comments are done via the [notes](notes.md) resource.
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index bea2b96c97a..404da3dc603 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -57,7 +57,6 @@ Example of response
"bio": null,
"created_at": "2015-12-21T13:14:24.077Z",
"id": 1,
- "is_admin": true,
"linkedin": "",
"name": "Administrator",
"skype": "",
@@ -101,7 +100,6 @@ Example of response
"bio": null,
"created_at": "2015-12-21T13:14:24.077Z",
"id": 1,
- "is_admin": true,
"linkedin": "",
"name": "Administrator",
"skype": "",
@@ -120,7 +118,7 @@ Example of response
Get a list of jobs for a pipeline.
```
-GET /projects/:id/pipeline/:pipeline_id/jobs
+GET /projects/:id/pipelines/:pipeline_id/jobs
```
| Attribute | Type | Required | Description |
@@ -173,7 +171,6 @@ Example of response
"bio": null,
"created_at": "2015-12-21T13:14:24.077Z",
"id": 1,
- "is_admin": true,
"linkedin": "",
"name": "Administrator",
"skype": "",
@@ -217,7 +214,6 @@ Example of response
"bio": null,
"created_at": "2015-12-21T13:14:24.077Z",
"id": 1,
- "is_admin": true,
"linkedin": "",
"name": "Administrator",
"skype": "",
@@ -284,7 +280,6 @@ Example of response
"bio": null,
"created_at": "2015-12-21T13:14:24.077Z",
"id": 1,
- "is_admin": true,
"linkedin": "",
"name": "Administrator",
"skype": "",
diff --git a/doc/api/keys.md b/doc/api/keys.md
index 3b55c2baf56..3ace1040f38 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -26,7 +26,6 @@ Parameters:
"avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon",
"web_url": "http://localhost:3000/john_smith",
"created_at": "2015-09-03T07:24:01.670Z",
- "is_admin": false,
"bio": null,
"skype": "",
"linkedin": "",
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index ff956add348..dde855b2bd4 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -11,15 +11,21 @@ GET /projects/:id/merge_requests
GET /projects/:id/merge_requests?state=opened
GET /projects/:id/merge_requests?state=all
GET /projects/:id/merge_requests?iids[]=42&iids[]=43
+GET /projects/:id/merge_requests?milestone=release
+GET /projects/:id/merge_requests?labels=bug,reproduced
```
Parameters:
-- `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`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `iids` | Array[integer] | no | Return the request having the given `iid` |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged`|
+| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
+| `milestone` | string | no | Return merge requests for a specific milestone |
+| `labels` | string | no | Return merge requests matching a comma separated list of labels |
```json
[
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 63f88a464f5..51de4fef7ff 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -859,6 +859,17 @@ Parameters:
| `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 |
+To upload a file from your filesystem, use the `--form` argument. This causes
+cURL to post data using the header `Content-Type: multipart/form-data`.
+The `file=` parameter must point to a file on your filesystem and be preceded
+by `@`. For example:
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v3/projects/5/uploads
+```
+
+Returned object:
+
```json
{
"alt": "dk",
@@ -868,8 +879,8 @@ Parameters:
```
**Note**: The returned `url` is relative to the project path.
-In Markdown contexts, the link is automatically expanded when the format in `markdown` is used.
-
+In Markdown contexts, the link is automatically expanded when the format in
+`markdown` is used.
## Project members
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/users.md b/doc/api/users.md
index 2ada4d09c84..86027bcc05c 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -62,7 +62,6 @@ GET /users
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z",
- "is_admin": false,
"bio": null,
"location": null,
"skype": "",
@@ -72,6 +71,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",
@@ -94,7 +94,6 @@ GET /users
"avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg",
"web_url": "http://localhost:3000/jack_smith",
"created_at": "2012-05-23T08:01:01Z",
- "is_admin": false,
"bio": null,
"location": null,
"skype": "",
@@ -104,6 +103,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 +130,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
@@ -155,7 +167,6 @@ Parameters:
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
"web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z",
- "is_admin": false,
"bio": null,
"location": null,
"skype": "",
@@ -186,7 +197,6 @@ Parameters:
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z",
- "is_admin": false,
"bio": null,
"location": null,
"skype": "",
@@ -196,6 +206,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",
@@ -310,7 +321,6 @@ GET /user
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z",
- "is_admin": false,
"bio": null,
"location": null,
"skype": "",
@@ -320,6 +330,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 +376,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 +998,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/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/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/README.md b/doc/development/README.md
index 3c797505aa9..77bb0263374 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -33,7 +33,6 @@
## Backend howtos
- [Architecture](architecture.md) of GitLab
-- [CI setup](ci_setup.md) for testing GitLab
- [Gotchas](gotchas.md) to avoid
- [How to dump production data to staging](db_dump.md)
- [Instrumentation](instrumentation.md)
diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md
deleted file mode 100644
index 0810b32efd7..00000000000
--- a/doc/development/ci_setup.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# CI setup
-
-This document describes what services we use for testing GitLab and GitLab CI.
-
-We currently use four CI services to test GitLab:
-
-1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce)
-2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org
-3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq)
-4. [Mock CI Service](../user/project/integrations/mock_ci.md) for local development
-
-| Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore |
-|---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------|
-| GitLab CE @ MySQL | ✓ | ✓ [Core team can trigger builds](https://gitlab-ce.githost.io/projects/4) | |
-| GitLab CE @ PostgreSQL | | | ✓ [Core team can trigger builds](https://semaphoreapp.com/gitlabhq/gitlabhq/branches/master) |
-| GitLab EE @ MySQL | ✓ | | |
-| GitLab CI @ MySQL | ✓ | | |
-| GitLab CI @ PostgreSQL | | | ✓ |
-| GitLab CI Runner | ✓ | | ✓ |
-| GitLab Shell | ✓ | | ✓ |
-| GitLab Shell | ✓ | | ✓ |
-
-Core team has access to trigger builds if needed for GitLab CE.
-
-We use [these build scripts](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) for testing with GitLab CI.
-
-# Build configuration on [Semaphore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for testing the [GitHub.com repo](https://github.com/gitlabhq/gitlabhq)
-
-- Language: Ruby
-- Ruby version: 2.1.8
-- database.yml: pg
-
-Build commands
-
-```bash
-sudo apt-get install cmake libicu-dev -y (Setup)
-bundle install --deployment --path vendor/bundle (Setup)
-cp config/gitlab.yml.example config/gitlab.yml (Setup)
-bundle exec rake db:create (Setup)
-bundle exec rake spinach (Thread #1)
-bundle exec rake spec (thread #2)
-bundle exec rake rubocop (thread #3)
-bundle exec rake brakeman (thread #4)
-bundle exec rake jasmine:ci (thread #5)
-```
-
-Use rubygems mirror.
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..a08694fb66a 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 one of Vue experts 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/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 a4631fd0073..157c13352ca 100644
--- a/doc/development/fe_guide/testing.md
+++ b/doc/development/fe_guide/testing.md
@@ -14,8 +14,10 @@ for more information on general testing practices at GitLab.
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.
+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`
@@ -26,6 +28,10 @@ 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
`rake karma` runs the frontend-only (JavaScript) tests.
@@ -134,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/testing.md b/doc/development/testing.md
index ad540ec13db..9b0b9808827 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -202,6 +202,7 @@ Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md).
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases.
- Try to use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
+- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
[four-phase-test]: https://robots.thoughtbot.com/four-phase-test
@@ -225,6 +226,20 @@ so we need to set some guidelines for their use going forward:
[lets-not]: https://robots.thoughtbot.com/lets-not
+#### `set` variables
+
+In some cases there is no need to recreate the same object for tests again for
+each example. For example, a project is needed to test issues on the same
+project, one project will do for the entire file. This can be achieved by using
+`set` in the same way you would use `let`.
+
+`rspec-set` only works on ActiveRecord objects, and before new examples it
+reloads or recreates the model, _only_ if needed. That is, when you changed
+properties or destroyed the object.
+
+There is one gotcha; you can't reference a model defined in a `let` block in a
+`set` block.
+
### Time-sensitive tests
[Timecop](https://github.com/travisjeffery/timecop) is available in our
@@ -448,13 +463,22 @@ 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.
+The GitLab test suite is [monitored] for the `master` branch, and any branch
+that includes `rspec-profile` in their name.
+
+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
+## CI setup
+
+- On CE, the test suite only runs against PostgreSQL by default. We additionally
+ run the suite against MySQL for tags, `master`, and any branch that includes
+ `mysql` in the name.
+- On EE, the test suite always runs both PostgreSQL and MySQL.
+
## Spinach (feature) tests
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index 482ec54207b..2814c18e0b6 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
@@ -70,3 +70,27 @@ All the docs follow the same [styleguide](doc_styleguide.md).
### Markdown
Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
+
+## Testing
+
+We try to treat documentation as code, thus have implemented some testing.
+Currently, the following tests are in place:
+
+1. `docs:check:links`: Check that all internal (relative) links work correctly
+1. `docs:check:apilint`: Check that the API docs follow some conventions
+
+If your contribution contains **only** documentation changes, you can speed up
+the CI process by prepending to the name of your branch: `docs/`. For example,
+a valid name would be `docs/update-api-issues` and it will run only the docs
+tests. If the name is `docs-update-api-issues`, the whole test suite will run
+(including docs).
+
+---
+
+When you submit a merge request to GitLab Community Edition (CE), there is an
+additional job called `rake ee_compat_check` that runs against Enterprise
+Edition (EE) and checks if your changes can apply cleanly to the EE codebase.
+If that job fails, read the instructions in the job log for what to do next.
+Contributors do not need to submit their changes to EE, GitLab Inc. employees
+on the other hand need to make sure that their changes apply cleanly to both
+CE and EE.
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 1c549844ee1..2513f4b420a 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -1,24 +1,28 @@
# How to create a project in GitLab
-There are two ways to create a new project in GitLab.
-
-1. While in your dashboard, you can create a new project using the **New project**
- green button or you can use the cross icon in the upper right corner next to
- your avatar which is always visible.
+1. In your dashboard, click the green **New project** button or use the plus
+ icon in the upper right corner of the navigation bar.
![Create a project](img/create_new_project_button.png)
-1. From there you can see several options.
+1. This opens the **New project** page.
![Project information](img/create_new_project_info.png)
-1. Fill out the information:
-
- 1. "Project name" is the name of your project (you can't use special characters,
- but you can use spaces, hyphens, underscores or even emojis).
- 1. The "Project description" is optional and will be shown in your project's
- dashboard so others can briefly understand what your project is about.
- 1. Select a [visibility level](../public_access/public_access.md).
- 1. You can also [import your existing projects](../workflow/importing/README.md).
-
-1. Finally, click **Create project**.
+1. Provide the following information:
+ - Enter the name of your project in the **Project name** field. You can't use
+ special characters, but you can use spaces, hyphens, underscores or even
+ emoji.
+ - If you have a project in a different repository, you can [import it] by
+ clicking an **Import project from** button provided this is enabled in
+ your GitLab instance. Ask your administrator if not.
+ - The **Project description (optional)** field enables you to enter a
+ description for your project's dashboard, which will help others
+ understand what your project is about. Though it's not required, it's a good
+ idea to fill this in.
+ - Changing the **Visibility Level** modifies the project's
+ [viewing and access rights](../public_access/public_access.md) for users.
+
+1. Click **Create project**.
+
+[import it]: ../workflow/importing/README.md
diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png
index 8d7a69e55ed..567f104880f 100644
--- a/doc/gitlab-basics/img/create_new_project_button.png
+++ b/doc/gitlab-basics/img/create_new_project_button.png
Binary files differ
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 1f61a4f67bb..b6bbc2a0af6 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -470,10 +470,8 @@ with setting up Gitaly until you upgrade to GitLab 9.2 or later.
sudo chmod 0700 /home/git/gitlab/tmp/sockets/private
sudo chown git /home/git/gitlab/tmp/sockets/private
- # Configure Gitaly
- cd /home/git/gitaly
- sudo -u git cp config.toml.example config.toml
# If you are using non-default settings you need to update config.toml
+ cd /home/git/gitaly
sudo -u git -H editor config.toml
# Enable Gitaly in the init script
diff --git a/doc/integration/chat_commands.md b/doc/integration/chat_commands.md
index 4b0084678d9..c878dc7e650 100644
--- a/doc/integration/chat_commands.md
+++ b/doc/integration/chat_commands.md
@@ -1,14 +1,14 @@
# Chat Commands
-Chat commands allow user to perform common operations on GitLab right from there chat client.
-Right now both Mattermost and Slack are supported.
+Chat commands in Mattermost and Slack (also called Slack slash commands) allow you to control GitLab and view GitLab content right inside your chat client, without having to leave it. For Slack, this requires a [project service configuration](../user/project/integrations/slack_slash_commands.md). Simply type the command as a message in your chat client to activate it.
-## Available commands
+Commands are scoped to a project, with a trigger term that is specified during configuration. (We suggest you use the project name as the trigger term for simplicty and clarity.) Taking the trigger term as `project-name`, the commands are:
-The trigger is configurable, but for the sake of this example, we'll use `/trigger`
-* `/trigger help` - Displays all available commands for this user
-* `/trigger issue new <title> <shift+return> <description>` - creates a new issue on the project
-* `/trigger issue show <id>` - Shows the issue with the given ID, if you've got access
-* `/trigger issue search <query>` - Shows a maximum of 5 items matching the query
-* `/trigger deploy <from> to <to>` - Deploy from an environment to another
+| Command | Effect |
+| ------- | ------ |
+| `/project-name help` | Shows all available chat commands |
+| `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` |
+| `/project-name issue show <id>` | Shows the issue with id `<id>` |
+| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` |
+| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment | \ No newline at end of file
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/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
index 8f9ef054949..2e7782736ff 100644
--- a/doc/migrate_ci_to_ce/README.md
+++ b/doc/migrate_ci_to_ce/README.md
@@ -1,4 +1,4 @@
-## Migrate GitLab CI to GitLab CE or EE
+# Migrate GitLab CI to GitLab CE or EE
Beginning with version 8.0 of GitLab Community Edition (CE) and Enterprise
Edition (EE), GitLab CI is no longer its own application, but is instead built
@@ -12,7 +12,7 @@ is not possible.**
We recommend that you read through the entire migration process in this
document before beginning.
-### Overview
+## Overview
In this document we assume you have a GitLab server and a GitLab CI server. It
does not matter if these are the same machine.
@@ -26,7 +26,7 @@ can be online for most of the procedure; the only GitLab downtime (if any) is
during the upgrade to 8.0. Your CI service will be offline from the moment you
upgrade to 8.0 until you finish the migration procedure.
-### Before upgrading
+## Before upgrading
If you have GitLab CI installed using omnibus-gitlab packages but **you don't want to migrate your existing data**:
@@ -38,12 +38,12 @@ run `sudo gitlab-ctl reconfigure` and you can reach CI at `gitlab.example.com/ci
If you want to migrate your existing data, continue reading.
-#### 0. Updating Omnibus from versions prior to 7.13
+### 0. Updating Omnibus from versions prior to 7.13
If you are updating from older versions you should first update to 7.14 and then to 8.0.
Otherwise it's pretty likely that you will encounter problems described in the [Troubleshooting](#troubleshooting).
-#### 1. Verify that backups work
+### 1. Verify that backups work
Make sure that the backup script on both servers can connect to the database.
@@ -73,7 +73,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production SKIP=r
If this fails you need to fix it before upgrading to 8.0. Also see
https://about.gitlab.com/getting-help/
-#### 2. Check source and target database types
+### 2. Check source and target database types
Check what databases you use on your GitLab server and your CI server.
Look for the 'adapter:' line. If your CI server and your GitLab server use
@@ -102,7 +102,7 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```
-#### 3. Storage planning
+### 3. Storage planning
Decide where to store CI build traces on GitLab server. GitLab CI uses
files on disk to store CI build traces. The default path for these build
@@ -111,34 +111,34 @@ traces is `/var/opt/gitlab/gitlab-ci/builds` (Omnibus) or
a special location, or if you are using NFS, you should make sure that you
store build traces on the same storage as your Git repositories.
-### I. Upgrading
+## I. Upgrading
From this point on, GitLab CI will be unavailable for your end users.
-#### 1. Upgrade GitLab to 8.0
+### 1. Upgrade GitLab to 8.0
First upgrade your GitLab server to version 8.0:
https://about.gitlab.com/update/
-#### 2. Disable CI on the GitLab server during the migration
+### 2. Disable CI on the GitLab server during the migration
After you update, go to the admin panel and temporarily disable CI. As
an administrator, go to **Admin Area** -> **Settings**, and under
**Continuous Integration** uncheck **Disable to prevent CI usage until rake
ci:migrate is run (8.0 only)**.
-#### 3. CI settings are now in GitLab
+### 3. CI settings are now in GitLab
If you want to use custom CI settings (e.g. change where builds are
stored), please update `/etc/gitlab/gitlab.rb` (Omnibus) or
`/home/git/gitlab/config/gitlab.yml` (Source).
-#### 4. Upgrade GitLab CI to 8.0
+### 4. Upgrade GitLab CI to 8.0
Now upgrade GitLab CI to version 8.0. If you are using Omnibus packages,
this may have already happened when you upgraded GitLab to 8.0.
-#### 5. Disable GitLab CI on the CI server
+### 5. Disable GitLab CI on the CI server
Disable GitLab CI after upgrading to 8.0.
@@ -154,9 +154,9 @@ cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec whenever --clear-crontab RAILS_ENV=production
```
-### II. Moving data
+## II. Moving data
-#### 1. Database encryption key
+### 1. Database encryption key
Move the database encryption key from your CI server to your GitLab
server. The command below will show you what you need to copy-paste to your
@@ -174,7 +174,7 @@ cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec rake backup:show_secrets RAILS_ENV=production
```
-#### 2. SQL data and build traces
+### 2. SQL data and build traces
Create your final CI data export. If you are converting from MySQL to
PostgreSQL, add ` MYSQL_TO_POSTGRESQL=1` to the end of the rake command. When
@@ -192,7 +192,7 @@ cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec rake backup:create RAILS_ENV=production
```
-#### 3. Copy data to the GitLab server
+### 3. Copy data to the GitLab server
If you were running GitLab and GitLab CI on the same server you can skip this
step.
@@ -209,7 +209,7 @@ ssh -A ci_admin@ci_server.example
scp /path/to/12345_gitlab_ci_backup.tar gitlab_admin@gitlab_server.example:~
```
-#### 4. Move data to the GitLab backups folder
+### 4. Move data to the GitLab backups folder
Make the CI data archive discoverable for GitLab. We assume below that you
store backups in the default path, adjust the command if necessary.
@@ -223,7 +223,7 @@ sudo mv /path/to/12345_gitlab_ci_backup.tar /var/opt/gitlab/backups/
sudo mv /path/to/12345_gitlab_ci_backup.tar /home/git/gitlab/tmp/backups/
```
-#### 5. Import the CI data into GitLab.
+### 5. Import the CI data into GitLab.
This step will delete any existing CI data on your GitLab server. There should
be no CI data yet because you turned CI on the GitLab server off earlier.
@@ -239,7 +239,7 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake ci:migrate RAILS_ENV=production
```
-#### 6. Restart GitLab
+### 6. Restart GitLab
```
# On your GitLab server:
@@ -251,7 +251,7 @@ sudo gitlab-ctl restart sidekiq
sudo service gitlab reload
```
-### III. Redirecting traffic
+## III. Redirecting traffic
If you were running GitLab CI with Omnibus packages and you were using the
internal NGINX configuration your CI service should now be available both at
@@ -261,7 +261,7 @@ If you installed GitLab CI from source we now need to configure a redirect in
NGINX so that existing CI runners can keep using the old CI server address, and
so that existing links to your CI server keep working.
-#### 1. Update Nginx configuration
+### 1. Update Nginx configuration
To ensure that your existing CI runners are able to communicate with the
migrated installation, and that existing build triggers still work, you'll need
@@ -317,22 +317,22 @@ You should also make sure that you can:
1. `curl https://YOUR_GITLAB_SERVER_FQDN/` from your previous GitLab CI server.
1. `curl https://YOUR_CI_SERVER_FQDN/` from your GitLab CE (or EE) server.
-#### 2. Check Nginx configuration
+### 2. Check Nginx configuration
sudo nginx -t
-#### 3. Restart Nginx
+### 3. Restart Nginx
sudo /etc/init.d/nginx restart
-#### Restore from backup
+### Restore from backup
If something went wrong and you need to restore a backup, consult the [Backup
restoration](../raketasks/backup_restore.md) guide.
-### Troubleshooting
+## Troubleshooting
-#### show:secrets problem (Omnibus-only)
+### show:secrets problem (Omnibus-only)
If you see errors like this:
```
Missing `secret_key_base` or `db_key_base` for 'production' environment. The secrets will be generated and stored in `config/secrets.yml`
@@ -343,7 +343,7 @@ Errno::EACCES: Permission denied @ rb_sysopen - config/secrets.yml
This can happen if you are updating from versions prior to 7.13 straight to 8.0.
The fix for this is to update to Omnibus 7.14 first and then update it to 8.0.
-#### Permission denied when accessing /var/opt/gitlab/gitlab-ci/builds
+### Permission denied when accessing /var/opt/gitlab/gitlab-ci/builds
To fix that issue you have to change builds/ folder permission before doing final backup:
```
sudo chown -R gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds
@@ -354,7 +354,7 @@ Then before executing `ci:migrate` you need to fix builds folder permission:
sudo chown git:git /var/opt/gitlab/gitlab-ci/builds
```
-#### Problems when importing CI database to GitLab
+### Problems when importing CI database to GitLab
If you were migrating CI database from MySQL to PostgreSQL manually you can see errors during import about missing sequences:
```
ALTER SEQUENCE
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 65fcfc77ab1..5be6053b76e 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -1,39 +1,52 @@
-# Backup restore
+# Backing up and restoring GitLab
![backup banner](backup_hrz.png)
An application data backup creates an archive file that contains the database,
all repositories and all attachments.
-This archive will be saved in `backup_path`, which is specified in the
-`config/gitlab.yml` file.
-The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP`
-identifies the time at which each backup was created.
-
-> In GitLab 8.15 we changed the timestamp format from `EPOCH` (`1393513186`)
-> to `EPOCH_YYYY_MM_DD` (`1393513186_2014_02_27`)
-You can only restore a backup to exactly the same version of GitLab on which it
-was created. The best way to migrate your repositories from one server to
+You can only restore a backup to **exactly the same version** of GitLab on which
+it was created. The best way to migrate your repositories from one server to
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.
+## Backup
+
+GitLab provides a simple command line interface to backup your whole installation,
+and is flexible enough to fit your needs.
+
+### Backup timestamp
+
+>**Note:**
+In GitLab 9.2 the timestamp format was changed from `EPOCH_YYYY_MM_DD` to
+`EPOCH_YYYY_MM_DD_GitLab version`, for example `1493107454_2017_04_25`
+would become `1493107454_2017_04_25_9.1.0`.
+
+The backup archive will be saved in `backup_path`, which is specified in the
+`config/gitlab.yml` file.
+The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP`
+identifies the time at which each backup was created, plus the GitLab version.
+The timestamp is needed if you need to restore GitLab and multiple backups are
+available.
+
+For example, if the backup name is `1493107454_2017_04_25_9.1.0_gitlab_backup.tar`,
+then the timestamp is `1493107454_2017_04_25_9.1.0`.
-## Create a backup of the GitLab system
+### Creating a backup of the GitLab system
Use this command if you've installed GitLab with the Omnibus package:
+
```
sudo gitlab-rake gitlab:backup:create
```
+
Use this if you've installed GitLab from source:
+
```
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
+
If you are running GitLab within a Docker container, you can run the backup from the host:
+
```
docker exec -t <container name> gitlab-rake gitlab:backup:create
```
@@ -67,9 +80,9 @@ Deleting tmp directories...[DONE]
Deleting old backups... [SKIPPING]
```
-## Backup Strategy Option
+### Backup strategy option
-> **Note:** Introduced as an option in 8.17
+> **Note:** Introduced as an option in GitLab 8.17.
The default backup strategy is to essentially stream data from the respective
data locations to the backup using the Linux command `tar` and `gzip`. This works
@@ -89,7 +102,7 @@ To use the `copy` strategy instead of the default streaming strategy, specify
`STRATEGY=copy` in the Rake task command. For example,
`sudo gitlab-rake gitlab:backup:create STRATEGY=copy`.
-## Exclude specific directories from the backup
+### Excluding specific directories from the backup
You can choose what should be backed up by adding the environment variable `SKIP`.
The available options are:
@@ -113,7 +126,7 @@ sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production
```
-## Upload backups to remote (cloud) storage
+### Uploading backups to a remote (cloud) storage
Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
It uses the [Fog library](http://fog.io/) to perform the upload.
@@ -257,7 +270,7 @@ For installations from source:
remote_directory: 'gitlab_backups'
```
-## Backup archive permissions
+### Backup archive permissions
The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`)
will have owner/group git:git and 0600 permissions by default.
@@ -275,11 +288,11 @@ gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives wo
archive_permissions: 0644 # Makes the backup archives world-readable
```
-## Storing configuration files
+### Storing configuration files
Please be informed that a backup does not store your configuration
-files. One reason for this is that your database contains encrypted
-information for two-factor authentication. Storing encrypted
+files. One reason for this is that your database contains encrypted
+information for two-factor authentication. Storing encrypted
information along with its key in the same place defeats the purpose
of using encryption in the first place!
@@ -292,11 +305,74 @@ At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and
`/home/git/gitlab/config/secrets.yml` (source) to preserve your database
encryption key.
-## Restore a previously created backup
+### Configuring cron to make daily backups
-You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1.
+>**Note:**
+The following cron jobs do not [backup your GitLab configuration files](#storing-configuration-files)
+or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
-### Prerequisites
+**For Omnibus installations**
+
+To schedule a cron job that backs up your repositories and GitLab metadata, use the root user:
+
+```
+sudo su -
+crontab -e
+```
+
+There, add the following line to schedule the backup for everyday at 2 AM:
+
+```
+0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1
+```
+
+You may also want to set a limited lifetime for backups to prevent regular
+backups using all your disk space. To do this add the following lines to
+`/etc/gitlab/gitlab.rb` and reconfigure:
+
+```
+# limit backup lifetime to 7 days - 604800 seconds
+gitlab_rails['backup_keep_time'] = 604800
+```
+
+Note that the `backup_keep_time` configuration option only manages local
+files. GitLab does not automatically prune old files stored in a third-party
+object storage (e.g., AWS S3) because the user may not have permission to list
+and delete files. We recommend that you configure the appropriate retention
+policy for your object storage. For example, you can configure [the S3 backup
+policy as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3).
+
+**For installation from source**
+
+```
+cd /home/git/gitlab
+sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups
+sudo -u git crontab -e # Edit the crontab for the git user
+```
+
+Add the following lines at the bottom:
+
+```
+# Create a full backup of the GitLab repositories and SQL database every day at 4am
+0 4 * * * cd /home/git/gitlab && PATH=/usr/local/bin:/usr/bin:/bin bundle exec rake gitlab:backup:create RAILS_ENV=production CRON=1
+```
+
+The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors.
+This is recommended to reduce cron spam.
+
+## Restore
+
+GitLab provides a simple command line interface to backup your whole installation,
+and is flexible enough to fit your needs.
+
+The [restore prerequisites section](#restore-prerequisites) includes crucial
+information. Make sure to read and test the whole restore process at least once
+before attempting to perform it in a production environment.
+
+You can only restore a backup to **exactly the same version** of GitLab that
+you created it on, for example 9.1.0.
+
+### Restore prerequisites
You need to have a working GitLab installation before you can perform
a restore. This is mainly because the system user performing the
@@ -305,13 +381,23 @@ the SQL database it needs to import data into ('gitlabhq_production').
All existing data will be either erased (SQL) or moved to a separate
directory (repositories, uploads).
-If some or all of your GitLab users are using two-factor authentication (2FA)
-then you must also make sure to restore `/etc/gitlab/gitlab.rb` and
-`/etc/gitlab/gitlab-secrets.json` (Omnibus), or
-`/home/git/gitlab/config/secrets.yml` (installations from source). Note that you
-need to run `gitlab-ctl reconfigure` after changing `gitlab-secrets.json`.
+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,
+[CI secret variables](../ci/variables/README.md#secret-variables), and
+secret variables used for [two-factor authentication](../user/profile/account/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.
+
+Depending on your case, you might want to run the restore command with one or
+more of the following options:
+
+- `BACKUP=timestamp_of_backup` - Required if more than one backup exists.
+ Read what the [backup timestamp is about](#backup-timestamp).
+- `force=yes` - Do not ask if the authorized_keys file should get regenerated.
-### Installation from source
+### Restore for installation from source
```
# Stop processes that are connected to the database
@@ -320,13 +406,6 @@ sudo service gitlab stop
bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
-Options:
-
-```
-BACKUP=timestamp_of_backup (required if more than one backup exists)
-force=yes (do not ask if the authorized_keys file should get regenerated)
-```
-
Example output:
```
@@ -358,13 +437,13 @@ Restoring repositories:
Deleting tmp directories...[DONE]
```
-### Omnibus installations
+### Restore for Omnibus installations
This procedure assumes that:
-- You have installed the exact same version of GitLab Omnibus with which the
- backup was created
-- You have run `sudo gitlab-ctl reconfigure` at least once
+- You have installed the **exact same version** of GitLab Omnibus with which the
+ backup was created.
+- You have run `sudo gitlab-ctl reconfigure` at least once.
- GitLab is running. If not, start it using `sudo gitlab-ctl start`.
First make sure your backup tar file is in the backup directory described in the
@@ -372,7 +451,7 @@ First make sure your backup tar file is in the backup directory described in the
`/var/opt/gitlab/backups`.
```shell
-sudo cp 1393513186_2014_02_27_gitlab_backup.tar /var/opt/gitlab/backups/
+sudo cp 1493107454_2017_04_25_9.1.0_gitlab_backup.tar /var/opt/gitlab/backups/
```
Stop the processes that are connected to the database. Leave the rest of GitLab
@@ -390,7 +469,7 @@ restore:
```shell
# This command will overwrite the contents of your GitLab database!
-sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186_2014_02_27
+sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2017_04_25_9.1.0
```
Restart and check GitLab:
@@ -402,59 +481,7 @@ sudo gitlab-rake gitlab:check SANITIZE=true
If there is a GitLab version mismatch between your backup tar file and the installed
version of GitLab, the restore command will abort with an error. Install the
-[correct GitLab version](https://about.gitlab.com/downloads/archives/) and try again.
-
-## Configure cron to make daily backups
-
-### For installation from source:
-```
-cd /home/git/gitlab
-sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups
-sudo -u git crontab -e # Edit the crontab for the git user
-```
-
-Add the following lines at the bottom:
-
-```
-# Create a full backup of the GitLab repositories and SQL database every day at 4am
-0 4 * * * cd /home/git/gitlab && PATH=/usr/local/bin:/usr/bin:/bin bundle exec rake gitlab:backup:create RAILS_ENV=production CRON=1
-```
-
-The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors.
-This is recommended to reduce cron spam.
-
-### For omnibus installations
-
-To schedule a cron job that backs up your repositories and GitLab metadata, use the root user:
-
-```
-sudo su -
-crontab -e
-```
-
-There, add the following line to schedule the backup for everyday at 2 AM:
-
-```
-0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1
-```
-
-You may also want to set a limited lifetime for backups to prevent regular
-backups using all your disk space. To do this add the following lines to
-`/etc/gitlab/gitlab.rb` and reconfigure:
-
-```
-# limit backup lifetime to 7 days - 604800 seconds
-gitlab_rails['backup_keep_time'] = 604800
-```
-
-Note that the `backup_keep_time` configuration option only manages local
-files. GitLab does not automatically prune old files stored in a third-party
-object storage (e.g. AWS S3) because the user may not have permission to list
-and delete files. We recommend that you configure the appropriate retention
-policy for your object storage. For example, you can configure [the S3 backup
-policy here as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3).
-
-NOTE: This cron job does not [backup your omnibus-gitlab configuration](#backup-and-restore-omnibus-gitlab-configuration) or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
+[correct GitLab version](https://packages.gitlab.com/gitlab/) and try again.
## Alternative backup strategies
@@ -479,6 +506,19 @@ Example: LVM snapshots + rsync
If you are running GitLab on a virtualized server you can possibly also create VM snapshots of the entire GitLab server.
It is not uncommon however for a VM snapshot to require you to power down the server, so this approach is probably of limited practical use.
+## Additional notes
+
+This documentation is for GitLab Community and Enterprise Edition. We backup
+GitLab.com and make sure your data is secure, but you can't use these methods
+to export / backup your data yourself from GitLab.com.
+
+Issues are stored in the database. They can't be stored in Git itself.
+
+To migrate your repositories from one server to another with an up-to-date version of
+GitLab, you can use the [import rake task](import.md) to do a mass import of the
+repository. Note that if you do an import rake task, rather than a backup restore, you
+will have all your repositories, but not any other data.
+
## Troubleshooting
### Restoring database backup using omnibus packages outputs warnings
@@ -488,7 +528,6 @@ If you are using backup restore procedures you might encounter the following war
psql:/var/opt/gitlab/backups/db/database.sql:22: ERROR: must be owner of extension plpgsql
psql:/var/opt/gitlab/backups/db/database.sql:2931: WARNING: no privileges could be revoked for "public" (two occurrences)
psql:/var/opt/gitlab/backups/db/database.sql:2933: WARNING: no privileges were granted for "public" (two occurrences)
-
```
Be advised that, backup is successfully restored in spite of these warnings.
@@ -497,14 +536,3 @@ The rake task runs this as the `gitlab` user which does not have the superuser a
Those objects have no influence on the database backup/restore but they give this annoying warning.
For more information see similar questions on postgresql issue tracker[here](http://www.postgresql.org/message-id/201110220712.30886.adrian.klaver@gmail.com) and [here](http://www.postgresql.org/message-id/2039.1177339749@sss.pgh.pa.us) as well as [stack overflow](http://stackoverflow.com/questions/4368789/error-must-be-owner-of-language-plpgsql).
-
-## Note
-This documentation is for GitLab CE.
-We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com.
-
-Issues are stored in the database. They can't be stored in Git itself.
-
-To migrate your repositories from one server to another with an up-to-date version of
-GitLab, you can use the [import rake task](import.md) to do a mass import of the
-repository. Note that if you do an import rake task, rather than a backup restore, you
-will have all your repositories, but not any other data.
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
index b99ba317a43..d13066c9015 100644
--- a/doc/topics/git/index.md
+++ b/doc/topics/git/index.md
@@ -17,6 +17,10 @@ We've gathered some resources to help you to get the best from Git with GitLab.
- [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)
+- Commits
+ - [Revert a commit](../../user/project/merge_requests/revert_changes.md#reverting-a-commit)
+ - [Cherry-picking a commit](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
+ - [Squashing commits](../../workflow/gitlab_flow.md#squashing-commits-with-rebase)
- **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/)
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/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index e5e3cd395df..e538983e603 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code
```bash
+cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index d6b3b0ffa5a..604166beb56 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code
```bash
+cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index ed0e668d854..d83965131f5 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code
```bash
+cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md
index aa1c659717e..aaadcec8ac0 100644
--- a/doc/update/8.13-to-8.14.md
+++ b/doc/update/8.13-to-8.14.md
@@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code
```bash
+cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md
index 1191662ee14..2d597894517 100644
--- a/doc/update/9.0-to-9.1.md
+++ b/doc/update/9.0-to-9.1.md
@@ -317,6 +317,17 @@ the socket path, but with `unix:` in front.
Each entry under `storages:` should use the same `gitaly_address`.
+#### Compile Gitaly
+
+This step will also create `config.toml.example` which you need below.
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
#### Gitaly config.toml
In GitLab 9.1 we are replacing environment variables in Gitaly with a
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/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md
index eac57bc3de4..a954840b8a6 100644
--- a/doc/user/admin_area/monitoring/health_check.md
+++ b/doc/user/admin_area/monitoring/health_check.md
@@ -1,36 +1,78 @@
# Health Check
-> [Introduced][ce-3888] in GitLab 8.8.
-
-GitLab provides a health check endpoint for uptime monitoring on the `health_check` web
-endpoint. The health check reports on the overall system status based on the status of
-the database connection, the state of the database migrations, and the ability to write
-and access the cache. This endpoint can be provided to uptime monitoring services like
-[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health].
+>**Notes:**
+ - Liveness and readiness probes were [introduced][ce-10416] in GitLab 9.1.
+ - The `health_check` endpoint was [introduced][ce-3888] in GitLab 8.8 and will
+ be deprecated in GitLab 9.1. Read more in the [old behavior](#old-behavior)
+ section.
+
+GitLab provides liveness and readiness probes to indicate service health and
+reachability to required services. These probes report on the status of the
+database connection, Redis connection, and access to the filesystem. These
+endpoints [can be provided to schedulers like Kubernetes][kubernetes] to hold
+traffic until the system is ready or restart the container as needed.
## Access Token
-An access token needs to be provided while accessing the health check endpoint. The current
-accepted token can be found on the `admin/health_check` page of your GitLab instance.
+An access token needs to be provided while accessing the probe endpoints. The current
+accepted token can be found under the **Admin area ➔ Monitoring ➔ Health check**
+(`admin/health_check`) page of your GitLab instance.
![access token](img/health_check_token.png)
The access token can be passed as a URL parameter:
```
-https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN
+https://gitlab.example.com/-/readiness?token=ACCESS_TOKEN
```
-or as an HTTP header:
+which will then provide a report of system health in JSON format:
-```bash
-curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+```
+{
+ "db_check": {
+ "status": "ok"
+ },
+ "redis_check": {
+ "status": "ok"
+ },
+ "fs_shards_check": {
+ "status": "ok",
+ "labels": {
+ "shard": "default"
+ }
+ }
+}
```
## Using the Endpoint
-Once you have the access token, health information can be retrieved as plain text, JSON,
-or XML using the `health_check` endpoint:
+Once you have the access token, the probes can be accessed:
+
+- `https://gitlab.example.com/-/readiness?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/-/liveness?token=ACCESS_TOKEN`
+
+## Status
+
+On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint
+will return a valid successful HTTP status code, and a `success` message.
+
+## Old behavior
+
+>**Notes:**
+ - Liveness and readiness probes were [introduced][ce-10416] in GitLab 9.1.
+ - The `health_check` endpoint was [introduced][ce-3888] in GitLab 8.8 and will
+ be deprecated in GitLab 9.1. Read more in the [old behavior](#old-behavior)
+ section.
+
+GitLab provides a health check endpoint for uptime monitoring on the `health_check` web
+endpoint. The health check reports on the overall system status based on the status of
+the database connection, the state of the database migrations, and the ability to write
+and access the cache. This endpoint can be provided to uptime monitoring services like
+[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health].
+
+Once you have the [access token](#access-token), health information can be
+retrieved as plain text, JSON, or XML using the `health_check` endpoint:
- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN`
- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN`
@@ -54,13 +96,13 @@ would be like:
{"healthy":true,"message":"success"}
```
-## Status
-
On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint
will return a valid successful HTTP status code, and a `success` message. Ideally your
uptime monitoring should look for the success message.
+[ce-10416]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10416
[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888
[pingdom]: https://www.pingdom.com
[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html
[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring
+[kubernetes]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/
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..733e70ca9bf
--- /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-out, you can 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..59e343ebe51
--- /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 edit 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/markdown.md b/doc/user/markdown.md
index 97de428d11d..0d29b471d52 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -431,7 +431,7 @@ Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
-Combined emphasis with **_asterisks and underscores_**.
+Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
```
@@ -640,10 +640,11 @@ Here's a line for us to start with.
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
This line is also a separate paragraph, but...
-This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
+This line is only separated by a single newline, so it *does not break* and just follows the previous line in the *same paragraph*.
+
+This line is also a separate paragraph, and...
+This line is *on its own line*, because the previous line ends with two spaces. (but still in the *same paragraph*)
-This line is also a separate paragraph, and...
-This line is on its own line, because the previous line ends with two
spaces.
```
@@ -651,11 +652,12 @@ Here's a line for us to start with.
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
-This line is also begins a separate paragraph, but...
-This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
+This line is also a separate paragraph, but...
+This line is only separated by a single newline, so it *does not break* and just follows the previous line in the *same paragraph*.
+
+This line is also a separate paragraph, and...
+This line is *on its own line*, because the previous line ends with two spaces. (but still in the *same paragraph*)
-This line is also a separate paragraph, and...
-This line is on its own line, because the previous line ends with two
spaces.
### Tables
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/delete_account.md b/doc/user/profile/account/delete_account.md
index 505248536c8..b5d3b009044 100644
--- a/doc/user/profile/account/delete_account.md
+++ b/doc/user/profile/account/delete_account.md
@@ -1,7 +1,7 @@
# Deleting a User Account
- As a user, you can delete your own account by navigating to **Settings** > **Account** and selecting **Delete account**
-- As an admin, you can delete a user account by navigating to the **Admin Area**, selecting the **Users** tab, selecting a user, and clicking on **Remvoe user**
+- As an admin, you can delete a user account by navigating to the **Admin Area**, selecting the **Users** tab, selecting a user, and clicking on **Remove user**
## Associated Records
diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md
index cad4757f287..1e28646bc97 100644
--- a/doc/user/project/integrations/bamboo.md
+++ b/doc/user/project/integrations/bamboo.md
@@ -51,9 +51,9 @@ service in GitLab.
## Troubleshooting
-If builds are not triggered, these are a couple of things to keep in mind.
+If builds are not triggered, ensure you entered the right GitLab IP address in
+Bamboo under 'Trigger IP addresses'.
+
+>**Note:**
+- Starting with GitLab 8.14.0, builds are triggered on push events.
-1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger
- IP addresses'.
-1. Remember that GitLab only triggers builds on push events. A commit via the
- web interface will not trigger CI currently.
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
index f7d5e3a8ab2..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
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..31baea507d7 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -47,9 +47,10 @@ 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 |
+| [Slack Notifications](slack.md) | Send GitLab events (e.g. issue created) to Slack as notifications |
+| [Slack slash commands](slack_slash_commands.md) | Use slash commands in Slack to control GitLab |
| PivotalTracker | Project Management Software (Source Commits Endpoint) |
| [Prometheus](prometheus.md) | Monitor the performance of your deployed apps |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
index e8b238351ca..af4ca35a215 100644
--- a/doc/user/project/integrations/slack.md
+++ b/doc/user/project/integrations/slack.md
@@ -1,51 +1,26 @@
# Slack Notifications Service
-## On Slack
+The Slack Notifications Service allows your GitLab project to send events (e.g. issue created) to your existing Slack team as notifications. This requires configurations in both Slack and GitLab.
-To enable Slack integration you must create an incoming webhook integration on
-Slack:
+> Note: You can also use Slack slash commands to control GitLab inside Slack. This is the separately configured [Slack slash commands](slack_slash_commands.md).
-1. [Sign in to Slack](https://slack.com/signin)
-1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
-1. Choose the channel name you want to send notifications to.
-1. Click **Add Incoming WebHooks Integration**
-1. Copy the **Webhook URL**, we'll need this later for GitLab.
+## Slack Configuration
-## On GitLab
+1. Sign in to your Slack team and [start a new Incoming WebHooks configuration](https://my.slack.com/services/new/incoming-webhook/).
+1. Select the Slack channel where notifications will be sent to by default. Click the **Add Incoming WebHooks integration** button to add the configuration.
+1. Copy the **Webhook URL**, which we'll use later in the GitLab configuration.
-After you set up Slack, it's time to set up GitLab.
+## GitLab Configuration
-Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
-and select the **Slack notifications** service to configure it.
-There, you will see a checkbox with the following events that can be triggered:
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) in your project's settings, i.e. **Project > Settings > Integrations**.
+1. Select the **Slack notifications** project service to configure it.
+1. Check the **Active** checkbox to turn on the service.
+1. Check the checkboxes corresponding to the GitLab events you want to send to Slack as a notification.
+1. For each event, optionally enter the Slack channel where you want to send the event. (Do _not_ include the `#` symbol.) If left empty, the event will be sent to the default channel that you configured in the Slack Configuration step.
+1. Paste the **Webhook URL** that you copied from the Slack Configuration step.
+1. Optionally customize the Slack bot username that will be sending the notifications.
+1. Configure the remaining options and click `Save changes`.
-- Push
-- Issue
-- Confidential issue
-- Merge request
-- Note
-- Tag push
-- Pipeline
-- Wiki page
+Your Slack team will now start receiving GitLab event notifications as configured.
-Below each of these event checkboxes, you have an input field to enter
-which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`).
-
-At the end, fill in your Slack details:
-
-| Field | Description |
-| ----- | ----------- |
-| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
-| **Username** | Optional username which can be on messages sent to Slack. Fill this in if you want to change the username of the bot. |
-| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
-
-After you are all done, click **Save changes** for the changes to take effect.
-
->**Note:**
-You can set "branch,pushed,Compare changes" as highlight words on your Slack
-profile settings, so that you can be aware of new commits when somebody pushes
-them.
-
-![Slack configuration](img/slack_configuration.png)
-
-[slackhook]: https://my.slack.com/services/new/incoming-webhook
+![Slack configuration](img/slack_configuration.png) \ No newline at end of file
diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md
index 56f1ba7311e..54e0ee611cb 100644
--- a/doc/user/project/integrations/slack_slash_commands.md
+++ b/doc/user/project/integrations/slack_slash_commands.md
@@ -2,23 +2,22 @@
> Introduced in GitLab 8.15
-Slack commands give users an extra interface to perform common operations
-from the chat environment. This allows one to, for example, create an issue as
-soon as the idea was discussed in chat.
-For all available commands try the help subcommand, for example: `/gitlab help`,
-all review the [full list of commands](../../../integration/chat_commands.md).
+Slack slash commands (also known as chat commmands) allow you to control GitLab and view content right inside Slack, without having to leave it. This requires configurations in both Slack and GitLab.
-## Prerequisites
-
-A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in
-Slack should be created beforehand, GitLab cannot create it for you.
+> Note: GitLab can also send events (e.g. issue created) to Slack as notifications. This is the separately configured [Slack Notifications Service](slack.md).
## Configuration
-Go to your project's [Integrations page](project_services.md#accessing-the-project-services)
-and select the **Slack slash commands** service to configure it.
+1. Slack slash commands are scoped to a project. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) in your project's settings, i.e. **Project > Settings > Integrations**.
+1. Select the **Slack slash commands** project service to configure it. This page contains required information to complete the configuration in Slack. Leave this browser tab open.
+1. Open a new browser tab and sign in to your Slack team. [Start a new Slash Commands integration](https://my.slack.com/services/new/slash-commands).
+1. Enter a trigger term. We suggest you use the project name. Click **Add Slash Command Integration**.
+1. Complete the rest of the fields in the Slack configuration page using information from the GitLab browser tab. In particular, the URL needs to be copied and pasted. Click **Save Integration** to complete the configuration in Slack.
+1. While still on the Slack configuration page, copy the **token**. Go back to the GitLab browser tab and paste in the **token**.
+1. Check the **Active** checkbox and click **Save changes** to complete the configuration in GitLab.
![Slack setup instructions](img/slack_setup.png)
-Once you've followed the instructions, mark the service as active and insert the token
-you've received from Slack. After saving the service you are good to go!
+## Usage
+
+You can now use the [Slack slash commands](../../../integration/chat_commands.md). \ No newline at end of file
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/project/slash_commands.md b/doc/user/project/slash_commands.md
index 45176fde9db..08452ca75cd 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -36,3 +36,4 @@ do.
| `/remove_time_spent` | Remove time spent |
| `/target_branch <Branch Name>` | Set target branch for current merge request |
| `/award :emoji:` | Toggle award for :emoji: |
+| `/board_move ~column` | Move issue to column on the board |
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/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/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/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/profile/profile.feature b/features/profile/profile.feature
index dc1339deb4c..70f47c97173 100644
--- a/features/profile/profile.feature
+++ b/features/profile/profile.feature
@@ -60,7 +60,9 @@ Feature: Profile
Then I should see a password error message
Scenario: I visit history tab
- Given I have activity
+ Given I logout
+ And I sign in via the UI
+ And I have activity
When I visit Audit Log page
Then I should see my activity
diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature
index 67f1e117f7f..9809b0ea0fe 100644
--- a/features/project/forked_merge_requests.feature
+++ b/features/project/forked_merge_requests.feature
@@ -41,8 +41,7 @@ Feature: Project Forked Merge Requests
@javascript
Scenario: I see the users in the target project for a new merge request
- Given I logout
- And I sign in as an admin
+ Given I sign in as an admin
And I have a project forked off of "Shop" called "Forked Shop"
Then I visit project "Forked Shop" merge requests page
And I click link "New Merge Request"
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/snippets.feature b/features/project/snippets.feature
index 3c51ea56585..50bc4c93df3 100644
--- a/features/project/snippets.feature
+++ b/features/project/snippets.feature
@@ -11,6 +11,7 @@ Feature: Project Snippets
Then I should see "Snippet one" in snippets
And I should not see "Snippet two" in snippets
+ @javascript
Scenario: I create new project snippet
Given I click link "New snippet"
And I submit new snippet "Snippet three"
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index 894c4a96bb8..472ec9544f3 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -10,7 +10,8 @@ Feature: Project Source Browse Files
Scenario: I browse files for specific ref
Given I visit project source page for "6d39438"
Then I should see files from repository for "6d39438"
-
+
+ @javascript
Scenario: I browse file content
Given I click on ".gitignore" file in repo
Then I should see its content
@@ -117,6 +118,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 +138,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 +268,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/source/markdown_render.feature b/features/project/source/markdown_render.feature
index ecbd721c281..fd583618dcf 100644
--- a/features/project/source/markdown_render.feature
+++ b/features/project/source/markdown_render.feature
@@ -6,11 +6,13 @@ Feature: Project Source Markdown Render
# Tree README
+ @javascript
Scenario: Tree view should have correct links in README
Given I go directory which contains README file
And I click on a relative link in README
Then I should see the correct markdown
+ @javascript
Scenario: I browse files from markdown branch
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
@@ -29,36 +31,42 @@ Feature: Project Source Markdown Render
And I click on GitLab API doc directory in README
Then I should see correct doc/api directory rendered
+ @javascript
Scenario: I view README in markdown branch to see reference links to file
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on Maintenance in README
Then I should see correct maintenance file rendered
+ @javascript
Scenario: README headers should have header links
Then I should see rendered README which contains correct links
And Header "Application details" should have correct id and link
# Blob
+ @javascript
Scenario: I navigate to doc directory to view documentation in markdown
And I navigate to the doc/api/README
And I see correct file rendered
And I click on users in doc/api/README
Then I should see the correct document file
+ @javascript
Scenario: I navigate to doc directory to view user doc in markdown
And I navigate to the doc/api/README
And I see correct file rendered
And I click on raketasks in doc/api/README
Then I should see correct directory rendered
+ @javascript
Scenario: I navigate to doc directory to view user doc in markdown
And I navigate to the doc/api/README
And Header "GitLab API" should have correct id and link
# Markdown branch
+ @javascript
Scenario: I browse files from markdown branch
When I visit markdown branch
Then I should see files from repository in markdown branch
@@ -73,6 +81,7 @@ Feature: Project Source Markdown Render
And I click on Rake tasks in README
Then I should see correct directory rendered for markdown branch
+ @javascript
Scenario: I navigate to doc directory to view documentation in markdown branch
When I visit markdown branch
And I navigate to the doc/api/README
@@ -80,6 +89,7 @@ Feature: Project Source Markdown Render
And I click on users in doc/api/README
Then I should see the users document file in markdown branch
+ @javascript
Scenario: I navigate to doc directory to view user doc in markdown branch
When I visit markdown branch
And I navigate to the doc/api/README
@@ -87,6 +97,7 @@ Feature: Project Source Markdown Render
And I click on raketasks in doc/api/README
Then I should see correct directory rendered for markdown branch
+ @javascript
Scenario: Tree markdown links view empty urls should have correct urls
When I visit markdown branch
Then The link with text "empty" should have url "tree/markdown"
@@ -99,6 +110,7 @@ Feature: Project Source Markdown Render
# "ID" means "#id" on the tests below, because we are unable to escape the hash sign.
# which Spinach interprets as the start of a comment.
+ @javascript
Scenario: All markdown links with ids should have correct urls
When I visit markdown branch
Then The link with text "ID" should have url "tree/markdownID"
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/snippets/snippets.feature b/features/snippets/snippets.feature
index e15d7c79342..1ad02780229 100644
--- a/features/snippets/snippets.feature
+++ b/features/snippets/snippets.feature
@@ -5,6 +5,7 @@ Feature: Snippets
And I have public "Personal snippet one" snippet
And I have private "Personal snippet private" snippet
+ @javascript
Scenario: I create new snippet
Given I visit new snippet page
And I submit new snippet "Personal snippet three"
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..f19fa1c7600 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -21,7 +21,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(response_headers['Content-Type']).to have_content("application/atom+xml")
expect(body).to have_selector("title", text: "#{@project.name}:master commits")
expect(body).to have_selector("author email", text: commit.author_email)
- expect(body).to have_selector("entry summary", text: commit.description[0..10].delete("\r"))
+ expect(body).to have_selector("entry summary", text: commit.description[0..10].delete("\r\n"))
end
step 'I click on tag link' do
@@ -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/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index ef1bb453615..8081b764be6 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -6,7 +6,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
include Select2Helper
step 'I am a member of project "Shop"' do
- @project = Project.find_by(name: "Shop")
+ @project = ::Project.find_by(name: "Shop")
@project ||= create(:project, :repository, name: "Shop")
@project.team << [@user, :reporter]
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/merge_requests.rb b/features/steps/project/merge_requests.rb
index 3985fe8f2f7..a06b2f2911f 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -48,8 +48,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see closed merge request "Bug NS-04"' do
- merge_request = MergeRequest.find_by!(title: "Bug NS-04")
- expect(merge_request).to be_closed
+ expect(page).to have_content "Bug NS-04"
expect(page).to have_content "Closed by"
end
diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb
index d7167352e02..7521a9439e3 100644
--- a/features/steps/project/merge_requests/acceptance.rb
+++ b/features/steps/project/merge_requests/acceptance.rb
@@ -43,7 +43,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
end
step 'I am signed in as a developer of the project' do
- login_as(@user)
+ sign_in(@user)
end
step 'I should see merge request merged' do
diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb
index a8f4e4ef027..1149c1c2426 100644
--- a/features/steps/project/merge_requests/revert.rb
+++ b/features/steps/project/merge_requests/revert.rb
@@ -31,7 +31,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
step 'I am signed in as a developer of the project' do
@user = create(:user) { |u| @project.add_developer(u) }
- login_as(@user)
+ sign_in(@user)
end
step 'There is an open Merge Request' do
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index a3bebfa4b71..60febd20104 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -3,6 +3,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
include SharedProject
include SharedNote
include SharedPaths
+ include WaitForAjax
step 'project "Shop" have "Snippet one" snippet' do
create(:project_snippet,
@@ -55,9 +56,10 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
fill_in "project_snippet_title", with: "Snippet three"
fill_in "project_snippet_file_name", with: "my_snippet.rb"
page.within('.file-editor') do
- find(:xpath, "//input[@id='project_snippet_content']").set 'Content of snippet three'
+ find('.ace_editor').native.send_keys 'Content of snippet three'
end
click_button "Create snippet"
+ wait_for_ajax
end
step 'I should see snippet "Snippet three"' do
@@ -79,6 +81,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
fill_in "note_note", with: "Good snippet!"
click_button "Comment"
end
+ wait_for_ajax
end
step 'I should see comment "Good snippet!"' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 5bd3c1a1246..ef09bddddd8 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -4,6 +4,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
include SharedProject
include SharedPaths
include RepoHelpers
+ include WaitForAjax
step "I don't have write access" do
@project = create(:project, :repository, name: "Other Project", path: "other-project")
@@ -36,10 +37,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I should see its content' do
+ wait_for_ajax
expect(page).to have_content old_gitignore_content
end
step 'I should see its new content' do
+ wait_for_ajax
expect(page).to have_content new_gitignore_content
end
@@ -87,9 +90,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 +287,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
@@ -360,7 +367,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I should see buttons for allowed commands' do
page.within '.content' do
- expect(page).to have_link 'Open raw'
+ expect(page).to have_link 'Download'
expect(page).to have_content 'History'
expect(page).to have_content 'Permalink'
expect(page).not_to have_content 'Edit'
@@ -373,7 +380,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/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index 115b67d98fb..abdbd795cd5 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -5,9 +5,10 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedMarkdown
+ include WaitForAjax
step 'I own project "Delta"' do
- @project = Project.find_by(name: "Delta")
+ @project = ::Project.find_by(name: "Delta")
@project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace)
@project.team << [@user, :master]
end
@@ -34,6 +35,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see correct document rendered' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
+ wait_for_ajax
expect(page).to have_content "All API requests require authentication"
end
@@ -63,6 +65,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see correct maintenance file rendered' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/raketasks/maintenance.md")
+ wait_for_ajax
expect(page).to have_content "bundle exec rake gitlab:env:info RAILS_ENV=production"
end
@@ -94,6 +97,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see correct file rendered' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
+ wait_for_ajax
expect(page).to have_content "Contents"
expect(page).to have_link "Users"
expect(page).to have_link "Rake tasks"
@@ -138,6 +142,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see correct file rendered in markdown branch' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
+ wait_for_ajax
expect(page).to have_content "Contents"
expect(page).to have_link "Users"
expect(page).to have_link "Rake tasks"
@@ -145,6 +150,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see correct document rendered for markdown branch' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md")
+ wait_for_ajax
expect(page).to have_content "All API requests require authentication"
end
@@ -162,6 +168,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
# Expected link contents
step 'The link with text "empty" should have url "tree/markdown"' do
+ wait_for_ajax
find('a', text: /^empty$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown")
end
@@ -197,6 +204,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
end
step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do
+ wait_for_ajax
find('a', text: /^#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") + '#id'
end
@@ -291,10 +299,12 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I should see the correct markdown' do
expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md")
+ wait_for_ajax
expect(page).to have_content "List users"
end
step 'Header "Application details" should have correct id and link' do
+ wait_for_ajax
header_should_have_correct_id_and_link(2, 'Application details', 'application-details')
end
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/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
index 4eef7aff213..8bae80a8707 100644
--- a/features/steps/shared/active_tab.rb
+++ b/features/steps/shared/active_tab.rb
@@ -1,5 +1,10 @@
module SharedActiveTab
include Spinach::DSL
+ include WaitForAjax
+
+ after do
+ wait_for_ajax if javascript_test?
+ end
def ensure_active_main_tab(content)
expect(find('.layout-nav li.active')).to have_content(content)
diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb
index 5c3e724746b..97fac595d8e 100644
--- a/features/steps/shared/authentication.rb
+++ b/features/steps/shared/authentication.rb
@@ -1,23 +1,33 @@
-require Rails.root.join('spec', 'support', 'login_helpers')
+require Rails.root.join('features', 'support', 'login_helpers')
module SharedAuthentication
include Spinach::DSL
include LoginHelpers
step 'I sign in as a user' do
- login_as :user
+ sign_out(@user) if @user
+
+ @user = create(:user)
+ sign_in(@user)
+ end
+
+ step 'I sign in via the UI' do
+ gitlab_sign_in(create(:user))
end
step 'I sign in as an admin' do
- login_as :admin
+ sign_out(@user) if @user
+
+ @user = create(:admin)
+ sign_in(@user)
end
step 'I sign in as "John Doe"' do
- login_with(user_exists("John Doe"))
+ gitlab_sign_in(user_exists("John Doe"))
end
step 'I sign in as "Mary Jane"' do
- login_with(user_exists("Mary Jane"))
+ gitlab_sign_in(user_exists("Mary Jane"))
end
step 'I should be redirected to sign in page' do
@@ -25,14 +35,41 @@ module SharedAuthentication
end
step "I logout" do
- logout
+ gitlab_sign_out
end
step "I logout directly" do
- logout_direct
+ gitlab_sign_out
end
def current_user
@user || User.reorder(nil).first
end
+
+ private
+
+ def gitlab_sign_in(user)
+ visit new_user_session_path
+
+ fill_in "user_login", with: user.email
+ fill_in "user_password", with: "12345678"
+ check 'user_remember_me'
+ click_button "Sign in"
+
+ @user = user
+ end
+
+ def gitlab_sign_out
+ return unless @user
+
+ if Capybara.current_driver == Capybara.javascript_driver
+ find('.header-user-dropdown-toggle').click
+ click_link 'Sign out'
+ expect(page).to have_button('Sign in')
+ else
+ sign_out(@user)
+ end
+
+ @user = nil
+ end
end
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
index 875d27d9383..6610b97ecb2 100644
--- a/features/steps/shared/markdown.rb
+++ b/features/steps/shared/markdown.rb
@@ -3,7 +3,7 @@ module SharedMarkdown
def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki")
node = find("#{parent} h#{level} a#user-content-#{id}")
- expect(node[:href]).to eq "##{id}"
+ expect(node[:href]).to end_with "##{id}"
# Work around a weird Capybara behavior where calling `parent` on a node
# returns the whole document, not the node's actual parent element
diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb
index 19366b11071..0b3e942a4fd 100644
--- a/features/steps/snippets/snippets.rb
+++ b/features/steps/snippets/snippets.rb
@@ -3,6 +3,7 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps
include SharedPaths
include SharedProject
include SharedSnippet
+ include WaitForAjax
step 'I click link "Personal snippet one"' do
click_link "Personal snippet one"
@@ -26,9 +27,10 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps
fill_in "personal_snippet_title", with: "Personal snippet three"
fill_in "personal_snippet_file_name", with: "my_snippet.rb"
page.within('.file-editor') do
- find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of snippet three'
+ find('.ace_editor').native.send_keys 'Content of snippet three'
end
click_button "Create snippet"
+ wait_for_ajax
end
step 'I submit new internal snippet' do
diff --git a/features/support/login_helpers.rb b/features/support/login_helpers.rb
new file mode 100644
index 00000000000..540ff25a4f2
--- /dev/null
+++ b/features/support/login_helpers.rb
@@ -0,0 +1,19 @@
+module LoginHelpers
+ # After inclusion, IntegrationHelpers calls these two methods that aren't
+ # supported by Spinach, so we perform the end results ourselves
+ class << self
+ def setup(*args)
+ Spinach.hooks.before_scenario do
+ Warden.test_mode!
+ end
+ end
+
+ def teardown(*args)
+ Spinach.hooks.after_scenario do
+ Warden.test_reset!
+ end
+ end
+ end
+
+ include Devise::Test::IntegrationHelpers
+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..6d6ccefe877 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -14,10 +14,15 @@ module API
class User < UserBasic
expose :created_at
- expose :admin?, as: :is_admin
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 +30,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
@@ -34,8 +40,9 @@ module API
expose :external
end
- class UserWithPrivateToken < UserPublic
+ class UserWithPrivateDetails < UserPublic
expose :private_token
+ expose :admin?, as: :is_admin
end
class Email < Grape::Entity
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/helpers.rb b/lib/api/helpers.rb
index ddff3c8c1e8..86bf567fe69 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -102,7 +102,7 @@ module API
end
def authenticate!
- unauthorized! unless current_user && can?(current_user, :access_api)
+ unauthorized! unless current_user && can?(initial_current_user, :access_api)
end
def authenticate_non_get!
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 810e5063996..718f936a1fc 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -60,6 +60,12 @@ module API
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
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 215bc03d0e9..ebed26dd178 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -40,6 +40,8 @@ module API
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] =
@@ -138,7 +140,7 @@ module API
begin
Gitlab::GitalyClient::Notifications.new(project.repository).post_receive
rescue GRPC::Unavailable => e
- render_api_error(e, 500)
+ render_api_error!(e, 500)
end
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 05423c17449..522f0f3be92 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -35,7 +35,7 @@ module API
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
@@ -219,6 +219,21 @@ module API
authorize!(:destroy_issue, issue)
issue.destroy
end
+
+ desc 'List merge requests closing issue' do
+ success Entities::MergeRequestBasic
+ end
+ params do
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
+ end
+ get ':id/issues/:issue_iid/closed_by' do
+ issue = find_project_issue(params[:issue_iid])
+
+ merge_request_ids = MergeRequestsClosingIssues.where(issue_id: issue).select(:merge_request_id)
+ merge_requests = MergeRequestsFinder.new(current_user, project_id: user_project.id).execute.where(id: merge_request_ids)
+
+ present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
+ end
end
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index cb7aec47cf0..710deba5ae3 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -20,6 +20,8 @@ module API
error!(errors[:validate_fork], 422)
elsif errors[:validate_branches].any?
conflict!(errors[:validate_branches])
+ elsif errors[:base].any?
+ error!(errors[:base], 422)
end
render_api_error!(errors, 400)
@@ -33,6 +35,17 @@ module API
end
end
+ def find_merge_requests(args = {})
+ args = params.merge(args)
+
+ args[:milestone_title] = args.delete(:milestone)
+ args[:label_name] = args.delete(:labels)
+
+ merge_requests = MergeRequestsFinder.new(current_user, args).execute.inc_notes_with_associations
+
+ merge_requests.reorder(args[:order_by] => args[:sort])
+ end
+
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'
@@ -57,23 +70,15 @@ module API
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return merge requests sorted in `asc` or `desc` order.'
optional :iids, type: Array[Integer], desc: 'The IID array of merge requests'
+ optional :milestone, type: String, desc: 'Return merge requests for a specific milestone'
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
use :pagination
end
get ":id/merge_requests" do
authorize! :read_merge_request, user_project
- merge_requests = user_project.merge_requests.inc_notes_with_associations
- merge_requests = filter_by_iid(merge_requests, params[:iids]) if params[:iids].present?
-
- merge_requests =
- case params[:state]
- when 'opened' then merge_requests.opened
- when 'closed' then merge_requests.closed
- when 'merged' then merge_requests.merged
- else merge_requests
- end
+ merge_requests = find_merge_requests(project_id: user_project.id)
- merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
end
@@ -197,14 +202,15 @@ module API
end
put ':id/merge_requests/:merge_request_iid/merge' do
merge_request = find_project_merge_request(params[:merge_request_iid])
+ merge_when_pipeline_succeeds = to_boolean(params[:merge_when_pipeline_succeeds])
# Merge request can not be merged
# because user dont have permissions to push into target branch
unauthorized! unless merge_request.can_be_merged_by?(current_user)
- not_allowed! unless merge_request.mergeable_state?
+ not_allowed! unless merge_request.mergeable_state?(skip_ci_check: merge_when_pipeline_succeeds)
- render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds)
if params[:sha] && merge_request.diff_head_sha != params[:sha]
render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
@@ -215,7 +221,7 @@ module API
should_remove_source_branch: params[:should_remove_source_branch]
}
- if params[:merge_when_pipeline_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
+ if merge_when_pipeline_succeeds && merge_request.head_pipeline && merge_request.head_pipeline.active?
::MergeRequests::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user, merge_params)
.execute(merge_request)
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 50842370947..db4b31b55bc 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -11,7 +11,7 @@ module API
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'
@@ -103,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?
@@ -205,7 +206,7 @@ module API
# CE
at_least_one_of_ce =
[
- :builds_enabled,
+ :jobs_enabled,
:container_registry_enabled,
:default_branch,
:description,
@@ -236,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/session.rb b/lib/api/session.rb
index 002ffd1d154..016415c3023 100644
--- a/lib/api/session.rb
+++ b/lib/api/session.rb
@@ -1,7 +1,7 @@
module API
class Session < Grape::API
desc 'Login to get token' do
- success Entities::UserWithPrivateToken
+ success Entities::UserWithPrivateDetails
end
params do
optional :login, type: String, desc: 'The username'
@@ -14,7 +14,7 @@ module API
return unauthorized! unless user
return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
- present user, with: Entities::UserWithPrivateToken
+ present user, with: Entities::UserWithPrivateDetails
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index eedc59f8636..40acaebf670 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -39,10 +39,13 @@ module API
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
@@ -51,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
@@ -427,7 +433,7 @@ module API
success Entities::UserPublic
end
get do
- present current_user, with: sudo? ? Entities::UserWithPrivateToken : Entities::UserPublic
+ present current_user, with: sudo? ? Entities::UserWithPrivateDetails : Entities::UserPublic
end
desc "Get the currently authenticated user's SSH keys" do
@@ -534,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/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index 3077240e650..1616142a619 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -23,6 +23,8 @@ module API
error!(errors[:validate_fork], 422)
elsif errors[:validate_branches].any?
conflict!(errors[:validate_branches])
+ elsif errors[:base].any?
+ error!(errors[:base], 422)
end
render_api_error!(errors, 400)
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index 4016ac76348..d97e5d98229 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -80,16 +80,32 @@ module Backup
'port' => '--port',
'socket' => '--socket',
'username' => '--user',
- 'encoding' => '--default-character-set'
+ 'encoding' => '--default-character-set',
+ # SSL
+ 'sslkey' => '--ssl-key',
+ 'sslcert' => '--ssl-cert',
+ 'sslca' => '--ssl-ca',
+ 'sslcapath' => '--ssl-capath',
+ 'sslcipher' => '--ssl-cipher'
}
args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact
end
def pg_env
- ENV['PGUSER'] = config["username"] if config["username"]
- ENV['PGHOST'] = config["host"] if config["host"]
- ENV['PGPORT'] = config["port"].to_s if config["port"]
- ENV['PGPASSWORD'] = config["password"].to_s if config["password"]
+ args = {
+ 'username' => 'PGUSER',
+ 'host' => 'PGHOST',
+ 'port' => 'PGPORT',
+ 'password' => 'PGPASSWORD',
+ # SSL
+ 'sslmode' => 'PGSSLMODE',
+ 'sslkey' => 'PGSSLKEY',
+ 'sslcert' => 'PGSSLCERT',
+ 'sslrootcert' => 'PGSSLROOTCERT',
+ 'sslcrl' => 'PGSSLCRL',
+ 'sslcompression' => 'PGSSLCOMPRESSION'
+ }
+ args.each { |opt, arg| ENV[arg] = config[opt].to_s if config[opt] }
end
def report_success(success)
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 7b4476fa4db..330cd963626 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -15,11 +15,10 @@ module Backup
s[:gitlab_version] = Gitlab::VERSION
s[:tar_version] = tar_version
s[:skipped] = ENV["SKIP"]
- tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d')}#{FILE_NAME_SUFFIX}"
+ tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{s[:gitlab_version]}#{FILE_NAME_SUFFIX}"
- Dir.chdir(Gitlab.config.backup.path) do
- File.open("#{Gitlab.config.backup.path}/backup_information.yml",
- "w+") do |file|
+ Dir.chdir(backup_path) do
+ File.open("#{backup_path}/backup_information.yml", "w+") do |file|
file << s.to_yaml.gsub(/^---\n/, '')
end
@@ -64,9 +63,9 @@ module Backup
$progress.print "Deleting tmp directories ... "
backup_contents.each do |dir|
- next unless File.exist?(File.join(Gitlab.config.backup.path, dir))
+ next unless File.exist?(File.join(backup_path, dir))
- if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir))
+ if FileUtils.rm_rf(File.join(backup_path, dir))
$progress.puts "done".color(:green)
else
puts "deleting tmp directory '#{dir}' failed".color(:red)
@@ -83,8 +82,8 @@ module Backup
if keep_time > 0
removed = 0
- Dir.chdir(Gitlab.config.backup.path) do
- Dir.glob("*#{FILE_NAME_SUFFIX}").each do |file|
+ Dir.chdir(backup_path) do
+ backup_file_list.each do |file|
next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/
timestamp = $1.to_i
@@ -107,18 +106,14 @@ module Backup
end
def unpack
- Dir.chdir(Gitlab.config.backup.path)
+ Dir.chdir(backup_path)
# check for existing backups in the backup dir
- file_list = Dir.glob("*#{FILE_NAME_SUFFIX}")
-
- if file_list.count == 0
- $progress.puts "No backups found in #{Gitlab.config.backup.path}"
+ if backup_file_list.empty?
+ $progress.puts "No backups found in #{backup_path}"
$progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}"
exit 1
- end
-
- if file_list.count > 1 && ENV["BACKUP"].nil?
+ elsif backup_file_list.many? && ENV["BACKUP"].nil?
$progress.puts 'Found more than one backup, please specify which one you want to restore:'
$progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup'
exit 1
@@ -127,7 +122,7 @@ module Backup
tar_file = if ENV['BACKUP'].present?
"#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
else
- file_list.first
+ backup_file_list.first
end
unless File.exist?(tar_file)
@@ -169,6 +164,14 @@ module Backup
private
+ def backup_path
+ Gitlab.config.backup.path
+ end
+
+ def backup_file_list
+ @backup_file_list ||= Dir.glob("*#{FILE_NAME_SUFFIX}")
+ end
+
def connect_to_remote_directory(connection_settings)
connection = ::Fog::Storage.new(connection_settings)
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index d6138816e70..6255a611dbe 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -53,7 +53,10 @@ module Banzai
# Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern
- @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
+ @emoji_pattern ||=
+ /(?<=[^[:alnum:]:]|\n|^)
+ :(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):
+ (?=[^[:alnum:]:]|$)/x
end
# Build a regexp that matches all valid unicode emojis names.
diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb
index 0b2b8bd7f4d..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.present?
- node.add_child(Nokogiri::XML::Text.new(" [#{issuable.state}]", doc))
+ 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/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb
index c5ce360e172..cbabf9156de 100644
--- a/lib/banzai/issuable_extractor.rb
+++ b/lib/banzai/issuable_extractor.rb
@@ -28,9 +28,13 @@ module Banzai
issue_parser = Banzai::ReferenceParser::IssueParser.new(project, user)
merge_request_parser = Banzai::ReferenceParser::MergeRequestParser.new(project, user)
- issue_parser.issues_for_nodes(nodes).merge(
+ issuables_for_nodes = issue_parser.issues_for_nodes(nodes).merge(
merge_request_parser.merge_requests_for_nodes(nodes)
)
+
+ # The project for the issue/MR might be pending for deletion!
+ # Filter them out because we don't care about them.
+ issuables_for_nodes.select { |node, issuable| issuable.project }
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/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 4a585996aa5..61849a40383 100644
--- a/lib/container_registry/path.rb
+++ b/lib/container_registry/path.rb
@@ -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/github/client.rb b/lib/github/client.rb
new file mode 100644
index 00000000000..e65d908d232
--- /dev/null
+++ b/lib/github/client.rb
@@ -0,0 +1,23 @@
+module Github
+ class Client
+ attr_reader :connection, :rate_limit
+
+ def initialize(options)
+ @connection = Faraday.new(url: options.fetch(:url)) do |faraday|
+ faraday.options.open_timeout = options.fetch(:timeout, 60)
+ faraday.options.timeout = options.fetch(:timeout, 60)
+ faraday.authorization 'token', options.fetch(:token)
+ faraday.adapter :net_http
+ end
+
+ @rate_limit = RateLimit.new(connection)
+ end
+
+ def get(url, query = {})
+ exceed, reset_in = rate_limit.get
+ sleep reset_in if exceed
+
+ Github::Response.new(connection.get(url, query))
+ end
+ end
+end
diff --git a/lib/github/collection.rb b/lib/github/collection.rb
new file mode 100644
index 00000000000..014b2038c4b
--- /dev/null
+++ b/lib/github/collection.rb
@@ -0,0 +1,29 @@
+module Github
+ class Collection
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def fetch(url, query = {})
+ return [] if url.blank?
+
+ Enumerator.new do |yielder|
+ loop do
+ response = client.get(url, query)
+ response.body.each { |item| yielder << item }
+
+ raise StopIteration unless response.rels.key?(:next)
+ url = response.rels[:next]
+ end
+ end.lazy
+ end
+
+ private
+
+ def client
+ @client ||= Github::Client.new(options)
+ end
+ end
+end
diff --git a/lib/github/error.rb b/lib/github/error.rb
new file mode 100644
index 00000000000..66d7afaa787
--- /dev/null
+++ b/lib/github/error.rb
@@ -0,0 +1,3 @@
+module Github
+ RepositoryFetchError = Class.new(StandardError)
+end
diff --git a/lib/github/import.rb b/lib/github/import.rb
new file mode 100644
index 00000000000..d49761fd6c6
--- /dev/null
+++ b/lib/github/import.rb
@@ -0,0 +1,409 @@
+require_relative 'error'
+module Github
+ class Import
+ include Gitlab::ShellAdapter
+
+ class MergeRequest < ::MergeRequest
+ self.table_name = 'merge_requests'
+
+ self.reset_callbacks :save
+ self.reset_callbacks :commit
+ self.reset_callbacks :update
+ self.reset_callbacks :validate
+ end
+
+ class Issue < ::Issue
+ self.table_name = 'issues'
+
+ self.reset_callbacks :save
+ self.reset_callbacks :commit
+ self.reset_callbacks :update
+ self.reset_callbacks :validate
+ end
+
+ class Note < ::Note
+ self.table_name = 'notes'
+
+ self.reset_callbacks :save
+ self.reset_callbacks :commit
+ self.reset_callbacks :update
+ self.reset_callbacks :validate
+ end
+
+ class LegacyDiffNote < ::LegacyDiffNote
+ self.table_name = 'notes'
+
+ self.reset_callbacks :commit
+ self.reset_callbacks :update
+ self.reset_callbacks :validate
+ end
+
+ attr_reader :project, :repository, :repo, :options, :errors, :cached, :verbose
+
+ def initialize(project, options)
+ @project = project
+ @repository = project.repository
+ @repo = project.import_source
+ @options = options
+ @verbose = options.fetch(:verbose, false)
+ @cached = Hash.new { |hash, key| hash[key] = Hash.new }
+ @errors = []
+ end
+
+ # rubocop: disable Rails/Output
+ def execute
+ puts 'Fetching repository...'.color(:aqua) if verbose
+ fetch_repository
+ puts 'Fetching labels...'.color(:aqua) if verbose
+ fetch_labels
+ puts 'Fetching milestones...'.color(:aqua) if verbose
+ fetch_milestones
+ puts 'Fetching pull requests...'.color(:aqua) if verbose
+ fetch_pull_requests
+ puts 'Fetching issues...'.color(:aqua) if verbose
+ fetch_issues
+ puts 'Cloning wiki repository...'.color(:aqua) if verbose
+ fetch_wiki_repository
+ puts 'Expiring repository cache...'.color(:aqua) if verbose
+ expire_repository_cache
+
+ true
+ rescue Github::RepositoryFetchError
+ false
+ ensure
+ keep_track_of_errors
+ end
+
+ private
+
+ def fetch_repository
+ begin
+ project.create_repository unless project.repository.exists?
+ project.repository.add_remote('github', "https://{options.fetch(:token)}@github.com/#{repo}.git")
+ project.repository.set_remote_as_mirror('github')
+ project.repository.fetch_remote('github', forced: true)
+ rescue Gitlab::Shell::Error => e
+ error(:project, "https://github.com/#{repo}.git", e.message)
+ raise Github::RepositoryFetchError
+ end
+ end
+
+ def fetch_wiki_repository
+ wiki_url = "https://{options.fetch(:token)}@github.com/#{repo}.wiki.git"
+ wiki_path = "#{project.path_with_namespace}.wiki"
+
+ unless project.wiki.repository_exists?
+ gitlab_shell.import_repository(project.repository_storage_path, wiki_path, wiki_url)
+ end
+ rescue Gitlab::Shell::Error => e
+ # GitHub error message when the wiki repo has not been created,
+ # this means that repo has wiki enabled, but have no pages. So,
+ # we can skip the import.
+ if e.message !~ /repository not exported/
+ errors(:wiki, wiki_url, e.message)
+ end
+ end
+
+ def fetch_labels
+ url = "/repos/#{repo}/labels"
+
+ while url
+ response = Github::Client.new(options).get(url)
+
+ response.body.each do |raw|
+ begin
+ representation = Github::Representation::Label.new(raw)
+
+ label = project.labels.find_or_create_by!(title: representation.title) do |label|
+ label.color = representation.color
+ end
+
+ cached[:label_ids][label.title] = label.id
+ rescue => e
+ error(:label, representation.url, e.message)
+ end
+ end
+
+ url = response.rels[:next]
+ end
+ end
+
+ def fetch_milestones
+ url = "/repos/#{repo}/milestones"
+
+ while url
+ response = Github::Client.new(options).get(url, state: :all)
+
+ response.body.each do |raw|
+ begin
+ milestone = Github::Representation::Milestone.new(raw)
+ next if project.milestones.where(iid: milestone.iid).exists?
+
+ project.milestones.create!(
+ iid: milestone.iid,
+ title: milestone.title,
+ description: milestone.description,
+ due_date: milestone.due_date,
+ state: milestone.state,
+ created_at: milestone.created_at,
+ updated_at: milestone.updated_at
+ )
+ rescue => e
+ error(:milestone, milestone.url, e.message)
+ end
+ end
+
+ url = response.rels[:next]
+ end
+ end
+
+ def fetch_pull_requests
+ url = "/repos/#{repo}/pulls"
+
+ while url
+ response = Github::Client.new(options).get(url, state: :all, sort: :created, direction: :asc)
+
+ response.body.each do |raw|
+ pull_request = Github::Representation::PullRequest.new(raw, options.merge(project: project))
+ merge_request = MergeRequest.find_or_initialize_by(iid: pull_request.iid, source_project_id: project.id)
+ next unless merge_request.new_record? && pull_request.valid?
+
+ begin
+ restore_branches(pull_request)
+
+ author_id = user_id(pull_request.author, project.creator_id)
+ description = format_description(pull_request.description, pull_request.author)
+
+ merge_request.attributes = {
+ iid: pull_request.iid,
+ title: pull_request.title,
+ description: description,
+ source_project: pull_request.source_project,
+ source_branch: pull_request.source_branch_name,
+ source_branch_sha: pull_request.source_branch_sha,
+ target_project: pull_request.target_project,
+ target_branch: pull_request.target_branch_name,
+ target_branch_sha: pull_request.target_branch_sha,
+ state: pull_request.state,
+ milestone_id: milestone_id(pull_request.milestone),
+ author_id: author_id,
+ assignee_id: user_id(pull_request.assignee),
+ created_at: pull_request.created_at,
+ updated_at: pull_request.updated_at
+ }
+
+ merge_request.save!(validate: false)
+ merge_request.merge_request_diffs.create
+
+ # Fetch review comments
+ review_comments_url = "/repos/#{repo}/pulls/#{pull_request.iid}/comments"
+ fetch_comments(merge_request, :review_comment, review_comments_url, LegacyDiffNote)
+
+ # Fetch comments
+ comments_url = "/repos/#{repo}/issues/#{pull_request.iid}/comments"
+ fetch_comments(merge_request, :comment, comments_url)
+ rescue => e
+ error(:pull_request, pull_request.url, e.message)
+ ensure
+ clean_up_restored_branches(pull_request)
+ end
+ end
+
+ url = response.rels[:next]
+ end
+ end
+
+ def fetch_issues
+ url = "/repos/#{repo}/issues"
+
+ while url
+ response = Github::Client.new(options).get(url, state: :all, sort: :created, direction: :asc)
+
+ response.body.each do |raw|
+ representation = Github::Representation::Issue.new(raw, options)
+
+ begin
+ # Every pull request is an issue, but not every issue
+ # is a pull request. For this reason, "shared" actions
+ # for both features, like manipulating assignees, labels
+ # and milestones, are provided within the Issues API.
+ if representation.pull_request?
+ next unless representation.has_labels?
+
+ merge_request = MergeRequest.find_by!(target_project_id: project.id, iid: representation.iid)
+ merge_request.update_attribute(:label_ids, label_ids(representation.labels))
+ else
+ next if Issue.where(iid: representation.iid, project_id: project.id).exists?
+
+ author_id = user_id(representation.author, project.creator_id)
+ issue = Issue.new
+ issue.iid = representation.iid
+ issue.project_id = project.id
+ issue.title = representation.title
+ issue.description = format_description(representation.description, representation.author)
+ issue.state = representation.state
+ issue.label_ids = label_ids(representation.labels)
+ issue.milestone_id = milestone_id(representation.milestone)
+ issue.author_id = author_id
+ issue.assignee_id = user_id(representation.assignee)
+ issue.created_at = representation.created_at
+ issue.updated_at = representation.updated_at
+ issue.save!(validate: false)
+
+ # Fetch comments
+ if representation.has_comments?
+ comments_url = "/repos/#{repo}/issues/#{issue.iid}/comments"
+ fetch_comments(issue, :comment, comments_url)
+ end
+ end
+ rescue => e
+ error(:issue, representation.url, e.message)
+ end
+ end
+
+ url = response.rels[:next]
+ end
+ end
+
+ def fetch_comments(noteable, type, url, klass = Note)
+ while url
+ comments = Github::Client.new(options).get(url)
+
+ ActiveRecord::Base.no_touching do
+ comments.body.each do |raw|
+ begin
+ representation = Github::Representation::Comment.new(raw, options)
+ author_id = user_id(representation.author, project.creator_id)
+
+ note = klass.new
+ note.project_id = project.id
+ note.noteable = noteable
+ note.note = format_description(representation.note, representation.author)
+ note.commit_id = representation.commit_id
+ note.line_code = representation.line_code
+ note.author_id = author_id
+ note.created_at = representation.created_at
+ note.updated_at = representation.updated_at
+ note.save!(validate: false)
+ rescue => e
+ error(type, representation.url, e.message)
+ end
+ end
+ end
+
+ url = comments.rels[:next]
+ end
+ end
+
+ def fetch_releases
+ url = "/repos/#{repo}/releases"
+
+ while url
+ response = Github::Client.new(options).get(url)
+
+ response.body.each do |raw|
+ representation = Github::Representation::Release.new(raw)
+ next unless representation.valid?
+
+ release = ::Release.find_or_initialize_by(project_id: project.id, tag: representation.tag)
+ next unless relese.new_record?
+
+ begin
+ release.description = representation.description
+ release.created_at = representation.created_at
+ release.updated_at = representation.updated_at
+ release.save!(validate: false)
+ rescue => e
+ error(:release, representation.url, e.message)
+ end
+ end
+
+ url = response.rels[:next]
+ end
+ end
+
+ def restore_branches(pull_request)
+ restore_source_branch(pull_request) unless pull_request.source_branch_exists?
+ restore_target_branch(pull_request) unless pull_request.target_branch_exists?
+ end
+
+ def restore_source_branch(pull_request)
+ repository.create_branch(pull_request.source_branch_name, pull_request.source_branch_sha)
+ end
+
+ def restore_target_branch(pull_request)
+ repository.create_branch(pull_request.target_branch_name, pull_request.target_branch_sha)
+ end
+
+ def remove_branch(name)
+ repository.delete_branch(name)
+ rescue Rugged::ReferenceError
+ errors << { type: :branch, url: nil, error: "Could not clean up restored branch: #{name}" }
+ end
+
+ def clean_up_restored_branches(pull_request)
+ return if pull_request.opened?
+
+ remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists?
+ remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
+ end
+
+ def label_ids(labels)
+ labels.map { |attrs| cached[:label_ids][attrs.fetch('name')] }.compact
+ end
+
+ def milestone_id(milestone)
+ return unless milestone.present?
+
+ project.milestones.select(:id).find_by(iid: milestone.iid)&.id
+ end
+
+ def user_id(user, fallback_id = nil)
+ return unless user.present?
+ return cached[:user_ids][user.id] if cached[:user_ids].key?(user.id)
+
+ gitlab_user_id = user_id_by_external_uid(user.id) || user_id_by_email(user.email)
+
+ cached[:gitlab_user_ids][user.id] = gitlab_user_id.present?
+ cached[:user_ids][user.id] = gitlab_user_id || fallback_id
+ end
+
+ def user_id_by_email(email)
+ return nil unless email
+
+ ::User.find_by_any_email(email)&.id
+ end
+
+ def user_id_by_external_uid(id)
+ return nil unless id
+
+ ::User.select(:id)
+ .joins(:identities)
+ .merge(::Identity.where(provider: :github, extern_uid: id))
+ .first&.id
+ end
+
+ def format_description(body, author)
+ return body if cached[:gitlab_user_ids][author.id]
+
+ "*Created by: #{author.username}*\n\n#{body}"
+ end
+
+ def expire_repository_cache
+ repository.expire_content_cache
+ end
+
+ def keep_track_of_errors
+ return unless errors.any?
+
+ project.update_column(:import_error, {
+ message: 'The remote data could not be fully imported.',
+ errors: errors
+ }.to_json)
+ end
+
+ def error(type, url, message)
+ errors << { type: type, url: Gitlab::UrlSanitizer.sanitize(url), error: message }
+ end
+ end
+end
diff --git a/lib/github/rate_limit.rb b/lib/github/rate_limit.rb
new file mode 100644
index 00000000000..884693d093c
--- /dev/null
+++ b/lib/github/rate_limit.rb
@@ -0,0 +1,27 @@
+module Github
+ class RateLimit
+ SAFE_REMAINING_REQUESTS = 100
+ SAFE_RESET_TIME = 500
+ RATE_LIMIT_URL = '/rate_limit'.freeze
+
+ attr_reader :connection
+
+ def initialize(connection)
+ @connection = connection
+ end
+
+ def get
+ response = connection.get(RATE_LIMIT_URL)
+
+ # GitHub Rate Limit API returns 404 when the rate limit is disabled
+ return false unless response.status != 404
+
+ body = Oj.load(response.body, class_cache: false, mode: :compat)
+ remaining = body.dig('rate', 'remaining').to_i
+ reset_in = body.dig('rate', 'reset').to_i
+ exceed = remaining <= SAFE_REMAINING_REQUESTS
+
+ [exceed, reset_in]
+ end
+ end
+end
diff --git a/lib/github/repositories.rb b/lib/github/repositories.rb
new file mode 100644
index 00000000000..c1c9448f305
--- /dev/null
+++ b/lib/github/repositories.rb
@@ -0,0 +1,19 @@
+module Github
+ class Repositories
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def fetch
+ Collection.new(options).fetch(repos_url)
+ end
+
+ private
+
+ def repos_url
+ '/user/repos'
+ end
+ end
+end
diff --git a/lib/github/representation/base.rb b/lib/github/representation/base.rb
new file mode 100644
index 00000000000..f26bdbdd546
--- /dev/null
+++ b/lib/github/representation/base.rb
@@ -0,0 +1,30 @@
+module Github
+ module Representation
+ class Base
+ def initialize(raw, options = {})
+ @raw = raw
+ @options = options
+ end
+
+ def id
+ raw['id']
+ end
+
+ def url
+ raw['url']
+ end
+
+ def created_at
+ raw['created_at']
+ end
+
+ def updated_at
+ raw['updated_at']
+ end
+
+ private
+
+ attr_reader :raw, :options
+ end
+ end
+end
diff --git a/lib/github/representation/branch.rb b/lib/github/representation/branch.rb
new file mode 100644
index 00000000000..d1dac6944f0
--- /dev/null
+++ b/lib/github/representation/branch.rb
@@ -0,0 +1,51 @@
+module Github
+ module Representation
+ class Branch < Representation::Base
+ attr_reader :repository
+
+ def user
+ raw.dig('user', 'login') || 'unknown'
+ end
+
+ def repo
+ return @repo if defined?(@repo)
+
+ @repo = Github::Representation::Repo.new(raw['repo']) if raw['repo'].present?
+ end
+
+ def ref
+ raw['ref']
+ end
+
+ def sha
+ raw['sha']
+ end
+
+ def short_sha
+ Commit.truncate_sha(sha)
+ end
+
+ def exists?
+ branch_exists? && commit_exists?
+ end
+
+ def valid?
+ sha.present? && ref.present?
+ end
+
+ private
+
+ def branch_exists?
+ repository.branch_exists?(ref)
+ end
+
+ def commit_exists?
+ repository.branch_names_contains(sha).include?(ref)
+ end
+
+ def repository
+ @repository ||= options.fetch(:repository)
+ end
+ end
+ end
+end
diff --git a/lib/github/representation/comment.rb b/lib/github/representation/comment.rb
new file mode 100644
index 00000000000..1b5be91461b
--- /dev/null
+++ b/lib/github/representation/comment.rb
@@ -0,0 +1,42 @@
+module Github
+ module Representation
+ class Comment < Representation::Base
+ def note
+ raw['body'] || ''
+ end
+
+ def author
+ @author ||= Github::Representation::User.new(raw['user'], options)
+ end
+
+ def commit_id
+ raw['commit_id']
+ end
+
+ def line_code
+ return unless on_diff?
+
+ parsed_lines = Gitlab::Diff::Parser.new.parse(diff_hunk.lines)
+ generate_line_code(parsed_lines.to_a.last)
+ end
+
+ private
+
+ def generate_line_code(line)
+ Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
+ end
+
+ def on_diff?
+ diff_hunk.present?
+ end
+
+ def diff_hunk
+ raw['diff_hunk']
+ end
+
+ def file_path
+ raw['path']
+ end
+ end
+ end
+end
diff --git a/lib/github/representation/issuable.rb b/lib/github/representation/issuable.rb
new file mode 100644
index 00000000000..9713b82615d
--- /dev/null
+++ b/lib/github/representation/issuable.rb
@@ -0,0 +1,37 @@
+module Github
+ module Representation
+ class Issuable < Representation::Base
+ def iid
+ raw['number']
+ end
+
+ def title
+ raw['title']
+ end
+
+ def description
+ raw['body'] || ''
+ end
+
+ def milestone
+ return unless raw['milestone'].present?
+
+ @milestone ||= Github::Representation::Milestone.new(raw['milestone'])
+ end
+
+ def author
+ @author ||= Github::Representation::User.new(raw['user'], options)
+ end
+
+ def assignee
+ return unless assigned?
+
+ @assignee ||= Github::Representation::User.new(raw['assignee'], options)
+ end
+
+ def assigned?
+ raw['assignee'].present?
+ end
+ end
+ end
+end
diff --git a/lib/github/representation/issue.rb b/lib/github/representation/issue.rb
new file mode 100644
index 00000000000..df3540a6e6c
--- /dev/null
+++ b/lib/github/representation/issue.rb
@@ -0,0 +1,25 @@
+module Github
+ module Representation
+ class Issue < Representation::Issuable
+ def labels
+ raw['labels']
+ end
+
+ def state
+ raw['state'] == 'closed' ? 'closed' : 'opened'
+ end
+
+ def has_comments?
+ raw['comments'] > 0
+ end
+
+ def has_labels?
+ labels.count > 0
+ end
+
+ def pull_request?
+ raw['pull_request'].present?
+ end
+ end
+ end
+end
diff --git a/lib/github/representation/label.rb b/lib/github/representation/label.rb
new file mode 100644
index 00000000000..60aa51f9569
--- /dev/null
+++ b/lib/github/representation/label.rb
@@ -0,0 +1,13 @@
+module Github
+ module Representation
+ class Label < Representation::Base
+ def color
+ "##{raw['color']}"
+ end
+
+ def title
+ raw['name']
+ end
+ end
+ end
+end
diff --git a/lib/github/representation/milestone.rb b/lib/github/representation/milestone.rb
new file mode 100644
index 00000000000..917e6394ad4
--- /dev/null
+++ b/lib/github/representation/milestone.rb
@@ -0,0 +1,25 @@
+module Github
+ module Representation
+ class Milestone < Representation::Base
+ def iid
+ raw['number']
+ end
+
+ def title
+ raw['title']
+ end
+
+ def description
+ raw['description']
+ end
+
+ def due_date
+ raw['due_on']
+ end
+
+ def state
+ raw['state'] == 'closed' ? 'closed' : 'active'
+ end
+ end
+ end
+end
diff --git a/lib/github/representation/pull_request.rb b/lib/github/representation/pull_request.rb
new file mode 100644
index 00000000000..ac9c8283b4b
--- /dev/null
+++ b/lib/github/representation/pull_request.rb
@@ -0,0 +1,78 @@
+module Github
+ module Representation
+ class PullRequest < Representation::Issuable
+ attr_reader :project
+
+ delegate :user, :repo, :ref, :sha, to: :source_branch, prefix: true
+ delegate :user, :exists?, :repo, :ref, :sha, :short_sha, to: :target_branch, prefix: true
+
+ def source_project
+ project
+ end
+
+ def source_branch_exists?
+ !cross_project? && source_branch.exists?
+ end
+
+ def source_branch_name
+ @source_branch_name ||=
+ if cross_project? || !source_branch_exists?
+ source_branch_name_prefixed
+ else
+ source_branch_ref
+ end
+ end
+
+ def target_project
+ project
+ end
+
+ def target_branch_name
+ @target_branch_name ||= target_branch_exists? ? target_branch_ref : target_branch_name_prefixed
+ end
+
+ def state
+ return 'merged' if raw['state'] == 'closed' && raw['merged_at'].present?
+ return 'closed' if raw['state'] == 'closed'
+
+ 'opened'
+ end
+
+ def opened?
+ state == 'opened'
+ end
+
+ def valid?
+ source_branch.valid? && target_branch.valid?
+ end
+
+ private
+
+ def project
+ @project ||= options.fetch(:project)
+ end
+
+ def source_branch
+ @source_branch ||= Representation::Branch.new(raw['head'], repository: project.repository)
+ end
+
+ def source_branch_name_prefixed
+ "gh-#{target_branch_short_sha}/#{iid}/#{source_branch_user}/#{source_branch_ref}"
+ end
+
+ def target_branch
+ @target_branch ||= Representation::Branch.new(raw['base'], repository: project.repository)
+ end
+
+ def target_branch_name_prefixed
+ "gl-#{target_branch_short_sha}/#{iid}/#{target_branch_user}/#{target_branch_ref}"
+ end
+
+ def cross_project?
+ return true if source_branch_repo.nil?
+
+ source_branch_repo.id != target_branch_repo.id
+ end
+ end
+ end
+end
diff --git a/lib/github/representation/release.rb b/lib/github/representation/release.rb
new file mode 100644
index 00000000000..e7e4b428c1a
--- /dev/null
+++ b/lib/github/representation/release.rb
@@ -0,0 +1,17 @@
+module Github
+ module Representation
+ class Release < Representation::Base
+ def description
+ raw['body']
+ end
+
+ def tag
+ raw['tag_name']
+ end
+
+ def valid?
+ !raw['draft']
+ end
+ end
+ end
+end
diff --git a/lib/github/representation/repo.rb b/lib/github/representation/repo.rb
new file mode 100644
index 00000000000..6938aa7db05
--- /dev/null
+++ b/lib/github/representation/repo.rb
@@ -0,0 +1,6 @@
+module Github
+ module Representation
+ class Repo < Representation::Base
+ end
+ end
+end
diff --git a/lib/github/representation/user.rb b/lib/github/representation/user.rb
new file mode 100644
index 00000000000..18591380e25
--- /dev/null
+++ b/lib/github/representation/user.rb
@@ -0,0 +1,15 @@
+module Github
+ module Representation
+ class User < Representation::Base
+ def email
+ return @email if defined?(@email)
+
+ @email = Github::User.new(username, options).get.fetch('email', nil)
+ end
+
+ def username
+ raw['login']
+ end
+ end
+ end
+end
diff --git a/lib/github/response.rb b/lib/github/response.rb
new file mode 100644
index 00000000000..761c524b553
--- /dev/null
+++ b/lib/github/response.rb
@@ -0,0 +1,25 @@
+module Github
+ class Response
+ attr_reader :raw, :headers, :status
+
+ def initialize(response)
+ @raw = response
+ @headers = response.headers
+ @status = response.status
+ end
+
+ def body
+ Oj.load(raw.body, class_cache: false, mode: :compat)
+ end
+
+ def rels
+ links = headers['Link'].to_s.split(', ').map do |link|
+ href, name = link.match(/<(.*?)>; rel="(\w+)"/).captures
+
+ [name.to_sym, href]
+ end
+
+ Hash[*links.flatten]
+ end
+ end
+end
diff --git a/lib/github/user.rb b/lib/github/user.rb
new file mode 100644
index 00000000000..f88a29e590b
--- /dev/null
+++ b/lib/github/user.rb
@@ -0,0 +1,24 @@
+module Github
+ class User
+ attr_reader :username, :options
+
+ def initialize(username, options)
+ @username = username
+ @options = options
+ end
+
+ def get
+ client.get(user_url).body
+ end
+
+ private
+
+ def client
+ @client ||= Github::Client.new(options)
+ end
+
+ def user_url
+ "/users/#{username}"
+ end
+ end
+end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index d575367d81a..fba80c7132e 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -14,28 +14,16 @@ module Gitlab
# Public: Converts the provided Asciidoc markup into HTML.
#
# input - the source text in Asciidoc format
- # context - a Hash with the template context:
- # :commit
- # :project
- # :project_wiki
- # :requested_path
- # :ref
- # asciidoc_opts - a Hash of options to pass to the Asciidoctor converter
#
- def self.render(input, context, asciidoc_opts = {})
- asciidoc_opts.reverse_merge!(
- safe: :secure,
- backend: :gitlab_html5,
- attributes: []
- )
- asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
+ def self.render(input)
+ asciidoc_opts = { safe: :secure,
+ backend: :gitlab_html5,
+ attributes: DEFAULT_ADOC_ATTRS }
plantuml_setup
html = ::Asciidoctor.convert(input, asciidoc_opts)
- html = Banzai.post_process(html, context)
-
filter = Banzai::Filter::SanitizationFilter.new(html)
html = filter.call.to_s
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index eee5601b0ed..ea918b23a63 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -108,7 +108,7 @@ module Gitlab
token = Doorkeeper::AccessToken.by_token(password)
if valid_oauth_token?(token)
user = User.find_by(id: token.resource_owner_id)
- Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)
+ Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities)
end
end
end
diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb
index b358f2efa4f..4fc9a075edc 100644
--- a/lib/gitlab/cache/ci/project_pipeline_status.rb
+++ b/lib/gitlab/cache/ci/project_pipeline_status.rb
@@ -15,18 +15,51 @@ module Gitlab
end
end
+ def self.load_in_batch_for_projects(projects)
+ cached_results_for_projects(projects).zip(projects).each do |result, project|
+ project.pipeline_status = new(project, result)
+ project.pipeline_status.load_status
+ end
+ end
+
+ def self.cached_results_for_projects(projects)
+ result = Gitlab::Redis.with do |redis|
+ redis.multi do
+ projects.each do |project|
+ cache_key = cache_key_for_project(project)
+ redis.exists(cache_key)
+ redis.hmget(cache_key, :sha, :status, :ref)
+ end
+ end
+ end
+
+ result.each_slice(2).map do |(cache_key_exists, (sha, status, ref))|
+ pipeline_info = { sha: sha, status: status, ref: ref }
+ { loaded_from_cache: cache_key_exists, pipeline_info: pipeline_info }
+ end
+ end
+
+ def self.cache_key_for_project(project)
+ "projects/#{project.id}/pipeline_status"
+ end
+
def self.update_for_pipeline(pipeline)
- new(pipeline.project,
- sha: pipeline.sha,
- status: pipeline.status,
- ref: pipeline.ref).store_in_cache_if_needed
+ pipeline_info = {
+ sha: pipeline.sha,
+ status: pipeline.status,
+ ref: pipeline.ref
+ }
+
+ new(pipeline.project, pipeline_info: pipeline_info).
+ store_in_cache_if_needed
end
- def initialize(project, sha: nil, status: nil, ref: nil)
+ def initialize(project, pipeline_info: {}, loaded_from_cache: nil)
@project = project
- @sha = sha
- @ref = ref
- @status = status
+ @sha = pipeline_info[:sha]
+ @ref = pipeline_info[:ref]
+ @status = pipeline_info[:status]
+ @loaded = loaded_from_cache
end
def has_status?
@@ -85,6 +118,8 @@ module Gitlab
end
def has_cache?
+ return self.loaded unless self.loaded.nil?
+
Gitlab::Redis.with do |redis|
redis.exists(cache_key)
end
@@ -95,7 +130,7 @@ module Gitlab
end
def cache_key
- "projects/#{project.id}/build_status"
+ self.class.cache_key_for_project(project)
end
end
end
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index b929bdd55bc..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,14 +14,7 @@ module Gitlab
def initialize
@stream = yield
- if @stream
- @stream.binmode
- # Ci::Ansi2html::Converter would read from @stream directly,
- # using @stream.each_line to be specific. It's safe to set
- # the encoding here because IO#seek(bytes) and IO#read(bytes)
- # are not characters based, so encoding doesn't matter to them.
- @stream.set_encoding(Encoding.default_external)
- end
+ @stream&.binmode
end
def valid?
@@ -68,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)
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index d76aa38f741..1ff34553f0a 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -41,7 +41,7 @@ module Gitlab
type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push'
# Hash to be passed as post_receive_data
- data = {
+ {
object_kind: type,
event_name: type,
before: oldrev,
@@ -61,16 +61,15 @@ module Gitlab
repository: project.hook_attrs.slice(:name, :url, :description, :homepage,
:git_http_url, :git_ssh_url, :visibility_level)
}
-
- data
end
# This method provide a sample data generated with
# existing project and commits to test webhooks
def build_sample(project, user)
- commits = project.repository.commits(project.default_branch, limit: 3)
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
- build(project, user, commits.last.id, commits.first.id, ref, commits)
+ commits = project.repository.commits(project.default_branch.to_s, limit: 3) rescue []
+
+ build(project, user, commits.last&.id, commits.first&.id, ref, commits)
end
def checkout_sha(repository, newrev, ref)
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index 329d12f13d1..0bd226ef050 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -15,6 +15,10 @@ module Gitlab
super.tap { |_| store_highlight_cache }
end
+ def real_size
+ @merge_request_diff.real_size
+ end
+
private
# Extracted method to highlight in the same iteration to the diff_collection.
diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb
index 4d04f867268..c7542a8fabc 100644
--- a/lib/gitlab/diff/position_tracer.rb
+++ b/lib/gitlab/diff/position_tracer.rb
@@ -82,7 +82,7 @@ module Gitlab
file_diff, old_line, new_line = results
- Position.new(
+ new_position = Position.new(
old_path: file_diff.old_path,
new_path: file_diff.new_path,
head_sha: new_diff_refs.head_sha,
@@ -91,6 +91,13 @@ module Gitlab
old_line: old_line,
new_line: new_line
)
+
+ # If a position is found, but is not actually contained in the diff, for example
+ # because it was an unchanged line in the context of a change that was undone,
+ # we cannot return this as a successful trace.
+ return unless new_position.diff_line(repository)
+
+ new_position
end
private
diff --git a/lib/gitlab/email/handler/base_handler.rb b/lib/gitlab/email/handler/base_handler.rb
index 3f6ace0311a..0bba433d04b 100644
--- a/lib/gitlab/email/handler/base_handler.rb
+++ b/lib/gitlab/email/handler/base_handler.rb
@@ -16,6 +16,10 @@ module Gitlab
def execute
raise NotImplementedError
end
+
+ def metrics_params
+ { handler: self.class.name }
+ end
end
end
end
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index b8ec9138c10..e7f91607e7e 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -1,4 +1,3 @@
-
require 'gitlab/email/handler/base_handler'
module Gitlab
@@ -37,6 +36,10 @@ module Gitlab
@project ||= Project.find_by_full_path(project_path)
end
+ def metrics_params
+ super.merge(project: project)
+ end
+
private
def create_issue
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
index 0e22f2189ee..31bb775c357 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
@@ -26,16 +28,16 @@ module Gitlab
record_name: 'comment')
end
+ def metrics_params
+ super.merge(project: project)
+ end
+
private
def author
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..df70a063330 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
@@ -17,6 +19,10 @@ module Gitlab
noteable.unsubscribe(sent_notification.recipient)
end
+ def metrics_params
+ super.merge(project: project)
+ end
+
private
def sent_notification
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index ec0529b5a4b..c270c0ea9ff 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -1,4 +1,3 @@
-
require_dependency 'gitlab/email/handler'
# Inspired in great part by Discourse's Email::Receiver
@@ -32,6 +31,8 @@ module Gitlab
raise UnknownIncomingEmail unless handler
+ Gitlab::Metrics.add_event(:receive_email, handler.metrics_params)
+
handler.execute
end
@@ -69,6 +70,8 @@ module Gitlab
# Handle emails from clients which append with commas,
# example clients are Microsoft exchange and iOS app
Gitlab::IncomingEmail.scan_fallback_references(references)
+ when nil
+ []
end
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 98fd4e78126..e8bb9e1f805 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -109,10 +109,6 @@ module Gitlab
@binary.nil? ? super : @binary == true
end
- def empty?
- !data || data == ''
- end
-
def data
encode! @data
end
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/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 d7dac9f6149..c3f0de76d01 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -122,13 +122,30 @@ module Gitlab
# Returns the number of valid branches
def branch_count
- rugged.branches.count do |ref|
- begin
- ref.name && ref.target # ensures the branch is valid
+ Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.count_branch_names
+ else
+ rugged.branches.count do |ref|
+ begin
+ ref.name && ref.target # ensures the branch is valid
- true
- rescue Rugged::ReferenceError
- false
+ true
+ rescue Rugged::ReferenceError
+ false
+ end
+ end
+ end
+ end
+ end
+
+ # Returns the number of valid tags
+ def tag_count
+ Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.count_tag_names
+ else
+ rugged.tags.count
end
end
end
@@ -451,7 +468,7 @@ module Gitlab
# Returns true is +from+ is direct ancestor to +to+, otherwise false
def is_ancestor?(from, to)
- Gitlab::GitalyClient::Commit.is_ancestor(self, from, to)
+ gitaly_commit_client.is_ancestor(from, to)
end
# Return an array of Diff objects that represent the diff
@@ -494,7 +511,9 @@ module Gitlab
# :contains is the commit contained by the refs from which to begin (SHA1 or name)
# :max_count is the maximum number of commits to fetch
# :skip is the number of commits to skip
- # :order is the commits order and allowed value is :date(default) or :topo
+ # :order is the commits order and allowed value is :none (default), :date, or :topo
+ # commit ordering types are documented here:
+ # http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
#
def find_commits(options = {})
actual_options = options.dup
@@ -522,11 +541,8 @@ module Gitlab
end
end
- if actual_options[:order] == :topo
- walker.sorting(Rugged::SORT_TOPO)
- else
- walker.sorting(Rugged::SORT_NONE)
- end
+ sort_type = rugged_sort_type(actual_options[:order])
+ walker.sorting(sort_type)
commits = []
offset = actual_options[:skip]
@@ -1273,6 +1289,22 @@ module Gitlab
def gitaly_ref_client
@gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self)
end
+
+ def gitaly_commit_client
+ @gitaly_commit_client ||= Gitlab::GitalyClient::Commit.new(self)
+ end
+
+ # Returns the `Rugged` sorting type constant for a given
+ # sort type key. Valid keys are `:none`, `:topo`, and `:date`
+ def rugged_sort_type(key)
+ @rugged_sort_types ||= {
+ none: Rugged::SORT_NONE,
+ topo: Rugged::SORT_TOPO,
+ date: Rugged::SORT_DATE
+ }
+
+ @rugged_sort_types.fetch(key, Rugged::SORT_NONE)
+ end
end
end
end
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/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
index b7f39f3ef0b..27db1e19bc1 100644
--- a/lib/gitlab/gitaly_client/commit.rb
+++ b/lib/gitlab/gitaly_client/commit.rb
@@ -5,6 +5,23 @@ module Gitlab
# See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
+ attr_accessor :stub
+
+ def initialize(repository)
+ @gitaly_repo = repository.gitaly_repository
+ @stub = Gitaly::Commit::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
+ end
+
+ def is_ancestor(ancestor_id, child_id)
+ request = Gitaly::CommitIsAncestorRequest.new(
+ repository: @gitaly_repo,
+ ancestor_id: ancestor_id,
+ child_id: child_id
+ )
+
+ @stub.commit_is_ancestor(request).value
+ end
+
class << self
def diff_from_parent(commit, options = {})
repository = commit.project.repository
@@ -20,18 +37,6 @@ module Gitlab
Gitlab::Git::DiffCollection.new(stub.commit_diff(request), options)
end
-
- def is_ancestor(repository, ancestor_id, child_id)
- gitaly_repo = repository.gitaly_repository
- stub = Gitaly::Commit::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
- request = Gitaly::CommitIsAncestorRequest.new(
- repository: gitaly_repo,
- ancestor_id: ancestor_id,
- child_id: child_id
- )
-
- stub.commit_is_ancestor(request).value
- end
end
end
end
diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb
index d3c0743db4e..2a5e8f73e55 100644
--- a/lib/gitlab/gitaly_client/ref.rb
+++ b/lib/gitlab/gitaly_client/ref.rb
@@ -34,6 +34,14 @@ module Gitlab
stub.find_ref_name(request).name
end
+ def count_tag_names
+ tag_names.count
+ end
+
+ def count_branch_names
+ branch_names.count
+ end
+
private
def consume_refs_response(response, prefix:)
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index b02b9737493..5ca3e6a95ca 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -1,7 +1,23 @@
module Gitlab
module GoogleCodeImport
class Importer
- attr_reader :project, :repo
+ attr_reader :project, :repo, :closed_statuses
+
+ NICE_LABEL_COLOR_HASH =
+ {
+ 'Status: New' => '#428bca',
+ 'Status: Accepted' => '#5cb85c',
+ 'Status: Started' => '#8e44ad',
+ 'Priority: Critical' => '#ffcfcf',
+ 'Priority: High' => '#deffcf',
+ 'Priority: Medium' => '#fff5cc',
+ 'Priority: Low' => '#cfe9ff',
+ 'Type: Defect' => '#d9534f',
+ 'Type: Enhancement' => '#44ad8e',
+ 'Type: Task' => '#4b6dd0',
+ 'Type: Review' => '#8e44ad',
+ 'Type: Other' => '#7f8c8d'
+ }.freeze
def initialize(project)
@project = project
@@ -161,45 +177,19 @@ module Gitlab
end
def nice_label_color(name)
- case name
- when /\AComponent:/
- "#fff39e"
- when /\AOpSys:/
- "#e2e2e2"
- when /\AMilestone:/
- "#fee3ff"
-
- when "Status: New"
- "#428bca"
- when "Status: Accepted"
- "#5cb85c"
- when "Status: Started"
- "#8e44ad"
-
- when "Priority: Critical"
- "#ffcfcf"
- when "Priority: High"
- "#deffcf"
- when "Priority: Medium"
- "#fff5cc"
- when "Priority: Low"
- "#cfe9ff"
-
- when "Type: Defect"
- "#d9534f"
- when "Type: Enhancement"
- "#44ad8e"
- when "Type: Task"
- "#4b6dd0"
- when "Type: Review"
- "#8e44ad"
- when "Type: Other"
- "#7f8c8d"
- when *@closed_statuses.map { |s| nice_status_name(s) }
- "#cfcfcf"
- else
- "#e2e2e2"
- end
+ NICE_LABEL_COLOR_HASH[name] ||
+ case name
+ when /\AComponent:/
+ '#fff39e'
+ when /\AOpSys:/
+ '#e2e2e2'
+ when /\AMilestone:/
+ '#fee3ff'
+ when *closed_statuses.map { |s| nice_status_name(s) }
+ '#cfcfcf'
+ else
+ '#e2e2e2'
+ end
end
def nice_label_name(name)
diff --git a/lib/gitlab/issuable_sorter.rb b/lib/gitlab/issuable_sorter.rb
new file mode 100644
index 00000000000..d392214867a
--- /dev/null
+++ b/lib/gitlab/issuable_sorter.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module IssuableSorter
+ class << self
+ def sort(project, issuables, &sort_key)
+ grouped_items = issuables.group_by do |issuable|
+ if issuable.project.id == project.id
+ :project_ref
+ elsif issuable.project.namespace.id == project.namespace.id
+ :namespace_ref
+ else
+ :full_ref
+ end
+ end
+
+ natural_sort_issuables(grouped_items[:project_ref], project) +
+ natural_sort_issuables(grouped_items[:namespace_ref], project) +
+ natural_sort_issuables(grouped_items[:full_ref], project)
+ end
+
+ private
+
+ def natural_sort_issuables(issuables, project)
+ VersionSorter.sort(issuables || []) do |issuable|
+ issuable.to_reference(project)
+ end
+ end
+ end
+ end
+end
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 6e42d8941fb..afd24b4dcc5 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::BuildService.new(nil, user_params).execute
+ Users::BuildService.new(nil, user_params).execute(skip_authorization: true)
end
def user_attributes
diff --git a/lib/gitlab/other_markup.rb b/lib/gitlab/other_markup.rb
index e67acf28c94..c2adc9aa10b 100644
--- a/lib/gitlab/other_markup.rb
+++ b/lib/gitlab/other_markup.rb
@@ -4,19 +4,11 @@ module Gitlab
# Public: Converts the provided markup into HTML.
#
# input - the source text in a markup format
- # context - a Hash with the template context:
- # :commit
- # :project
- # :project_wiki
- # :requested_path
- # :ref
#
- def self.render(file_name, input, context)
+ def self.render(file_name, input)
html = GitHub::Markup.render(file_name, input).
force_encoding(input.encoding)
- html = Banzai.post_process(html, context)
-
filter = Banzai::Filter::SanitizationFilter.new(html)
html = filter.call.to_s
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/template/dockerfile_template.rb b/lib/gitlab/template/dockerfile_template.rb
index d5d3e045a42..20b054b0bd8 100644
--- a/lib/gitlab/template/dockerfile_template.rb
+++ b/lib/gitlab/template/dockerfile_template.rb
@@ -8,7 +8,7 @@ module Gitlab
class << self
def extension
- 'Dockerfile'
+ '.Dockerfile'
end
def categories
@@ -18,7 +18,7 @@ module Gitlab
end
def base_dir
- Rails.root.join('vendor/dockerfile')
+ Rails.root.join('vendor/Dockerfile')
end
def finder(project = nil)
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_access.rb b/lib/gitlab/user_access.rb
index 54728e5ff0e..e46ff313654 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -44,9 +44,7 @@ module Gitlab
if ProtectedBranch.protected?(project, ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
- has_access = project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push)
-
- has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref)
+ project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push)
else
user.can?(:push_code, project)
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/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index e6e40f6945d..c551f939df1 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -168,7 +168,7 @@ module Gitlab
end
def secret_path
- Rails.root.join('.gitlab_workhorse_secret')
+ Gitlab.config.workhorse.secret_file
end
def set_key_and_notify(key, value, expire: nil, overwrite: true)
diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake
index 2301ec9b228..99b3168d9eb 100644
--- a/lib/tasks/brakeman.rake
+++ b/lib/tasks/brakeman.rake
@@ -2,7 +2,7 @@ desc 'Security check via brakeman'
task :brakeman do
# We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge
# requests are welcome!
- if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z))
+ if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb,app/controllers/unicorn_test_controller.rb -w3 -z))
puts 'Security check succeed'
else
puts 'Security check failed'
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/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 8079c6e416c..046780481ba 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -2,6 +2,8 @@ namespace :gitlab do
namespace :gitaly do
desc "GitLab | Install or upgrade gitaly"
task :install, [:dir] => :environment do |t, args|
+ require 'toml'
+
warn_user_is_not_gitlab
unless args.dir.present?
abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]")
@@ -16,6 +18,7 @@ namespace :gitlab do
command = status.zero? ? 'gmake' : 'make'
Dir.chdir(args.dir) do
+ create_gitaly_configuration
run_command!([command])
end
end
@@ -33,5 +36,39 @@ namespace :gitlab do
puts TOML.dump(storage: config)
end
+
+ private
+
+ # We cannot create config.toml files for all possible Gitaly configuations.
+ # For instance, if Gitaly is running on another machine then it makes no
+ # sense to write a config.toml file on the current machine. This method will
+ # only write a config.toml file in the most common and simplest case: the
+ # case where we have exactly one Gitaly process and we are sure it is
+ # running locally because it uses a Unix socket.
+ def create_gitaly_configuration
+ storages = []
+ address = nil
+
+ Gitlab.config.repositories.storages.each do |key, val|
+ if address
+ if address != val['gitaly_address']
+ raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address."
+ end
+ elsif URI(val['gitaly_address']).scheme != 'unix'
+ raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses."
+ else
+ address = val['gitaly_address']
+ end
+
+ storages << { name: key, path: val['path'] }
+ end
+
+ File.open("config.toml", "w") do |f|
+ f.puts TOML.dump(socket_path: address.sub(%r{\Aunix:}, ''), storages: storages)
+ end
+ rescue ArgumentError => e
+ puts "Skipping config.toml generation:"
+ puts e.message
+ end
end
end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index cb2adc81c9d..1b04e1350ed 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -5,7 +5,7 @@ namespace :gitlab do
end
def update(template)
- sub_dir = template.repo_url.match(/([a-z-]+)\.git\z/)[1]
+ sub_dir = template.repo_url.match(/([A-Za-z-]+)\.git\z/)[1]
dir = File.join(vendor_directory, sub_dir)
unless clone_repository(template.repo_url, dir)
@@ -45,7 +45,11 @@ namespace :gitlab do
Template.new(
"https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
/(\.{1,2}|LICENSE|CONTRIBUTING.md|Pages|autodeploy|\.gitlab-ci.yml)\z/
- )
+ ),
+ Template.new(
+ "https://gitlab.com/gitlab-org/Dockerfile.git",
+ /(\.{1,2}|LICENSE|CONTRIBUTING.md|\.Dockerfile)\z/
+ ),
].freeze
def vendor_directory
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index a9dad6a1bf0..bc76d7edc55 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -1,67 +1,5 @@
require 'benchmark'
require 'rainbow/ext/string'
-require_relative '../gitlab/shell_adapter'
-require_relative '../gitlab/github_import/importer'
-
-class NewImporter < ::Gitlab::GithubImport::Importer
- def execute
- # Same as ::Gitlab::GithubImport::Importer#execute, but showing some progress.
- puts 'Importing repository...'.color(:aqua)
- import_repository unless project.repository_exists?
-
- puts 'Importing labels...'.color(:aqua)
- import_labels
-
- puts 'Importing milestones...'.color(:aqua)
- import_milestones
-
- puts 'Importing pull requests...'.color(:aqua)
- import_pull_requests
-
- puts 'Importing issues...'.color(:aqua)
- import_issues
-
- puts 'Importing issue comments...'.color(:aqua)
- import_comments(:issues)
-
- puts 'Importing pull request comments...'.color(:aqua)
- import_comments(:pull_requests)
-
- puts 'Importing wiki...'.color(:aqua)
- import_wiki
-
- # Gitea doesn't have a Release API yet
- # See https://github.com/go-gitea/gitea/issues/330
- unless project.gitea_import?
- import_releases
- end
-
- handle_errors
-
- project.repository.after_import
- project.import_finish
-
- true
- end
-
- def import_repository
- begin
- raise 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
-
- project.create_repository
- 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)
- rescue => e
- # Expire cache to prevent scenarios such as:
- # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
- # 2. Retried import, repo is broken or not imported but +exists?+ still returns true
- project.repository.expire_content_cache if project.repository_exists?
-
- raise "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
- end
- end
-end
class GithubImport
def self.run!(*args)
@@ -69,14 +7,14 @@ class GithubImport
end
def initialize(token, gitlab_username, project_path, extras)
- @token = token
+ @options = { url: 'https://api.github.com', token: token, verbose: true }
@project_path = project_path
@current_user = User.find_by_username(gitlab_username)
@github_repo = extras.empty? ? nil : extras.first
end
def run!
- @repo = GithubRepos.new(@token, @current_user, @github_repo).choose_one!
+ @repo = GithubRepos.new(@options, @current_user, @github_repo).choose_one!
raise 'No repo found!' unless @repo
@@ -90,25 +28,24 @@ class GithubImport
private
def show_warning!
- puts "This will import GH #{@repo.full_name.bright} into GL #{@project_path.bright} as #{@current_user.name}"
+ puts "This will import GitHub #{@repo['full_name'].bright} into GitLab #{@project_path.bright} as #{@current_user.name}"
puts "Permission checks are ignored. Press any key to continue.".color(:red)
STDIN.getch
- puts 'Starting the import...'.color(:green)
+ puts 'Starting the import (this could take a while)'.color(:green)
end
def import!
- import_url = @project.import_url.gsub(/\:\/\/(.*@)?/, "://#{@token}@")
- @project.update(import_url: import_url)
-
@project.import_start
timings = Benchmark.measure do
- NewImporter.new(@project).execute
+ Github::Import.new(@project, @options).execute
end
puts "Import finished. Timings: #{timings}".color(:green)
+
+ @project.import_finish
end
def new_project
@@ -116,17 +53,17 @@ class GithubImport
namespace_path, _sep, name = @project_path.rpartition('/')
namespace = find_or_create_namespace(namespace_path)
- Project.create!(
- import_url: "https://#{@token}@github.com/#{@repo.full_name}.git",
+ Projects::CreateService.new(
+ @current_user,
name: name,
path: name,
- description: @repo.description,
- namespace: namespace,
+ description: @repo['description'],
+ namespace_id: namespace.id,
visibility_level: visibility_level,
import_type: 'github',
- import_source: @repo.full_name,
- creator: @current_user
- )
+ import_source: @repo['full_name'],
+ skip_wiki: @repo['has_wiki']
+ ).execute
end
end
@@ -134,7 +71,6 @@ class GithubImport
return @current_user.namespace if names == @current_user.namespace_path
return @current_user.namespace unless @current_user.can_create_group?
- names = params[:target_namespace].presence || names
full_path_namespace = Namespace.find_by_full_path(names)
return full_path_namespace if full_path_namespace
@@ -159,13 +95,13 @@ class GithubImport
end
def visibility_level
- @repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
+ @repo['private'] ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
end
end
class GithubRepos
- def initialize(token, current_user, github_repo)
- @token = token
+ def initialize(options, current_user, github_repo)
+ @options = options
@current_user = current_user
@github_repo = github_repo
end
@@ -174,17 +110,17 @@ class GithubRepos
return found_github_repo if @github_repo
repos.each do |repo|
- print "ID: #{repo[:id].to_s.bright} ".color(:green)
- puts "- Name: #{repo[:full_name]}".color(:green)
+ print "ID: #{repo['id'].to_s.bright}".color(:green)
+ print "\tName: #{repo['full_name']}\n".color(:green)
end
print 'ID? '.bright
- repos.find { |repo| repo[:id] == repo_id }
+ repos.find { |repo| repo['id'] == repo_id }
end
def found_github_repo
- repos.find { |repo| repo[:full_name] == @github_repo }
+ repos.find { |repo| repo['full_name'] == @github_repo }
end
def repo_id
@@ -192,11 +128,7 @@ class GithubRepos
end
def repos
- @repos ||= client.repos
- end
-
- def client
- @client ||= Gitlab::GithubImport::Client.new(@token, {})
+ Github::Repositories.new(@options).fetch
end
end
diff --git a/package.json b/package.json
index a17399ddb8f..9ed5e1a7475 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,11 @@
"js-cookie": "^2.1.3",
"jszip": "^3.1.3",
"jszip-utils": "^0.0.2",
+ "marked": "^0.3.6",
"mousetrap": "^1.4.6",
+ "pdfjs-dist": "^1.8.252",
"pikaday": "^1.5.1",
+ "prismjs": "^1.6.0",
"raphael": "^2.2.7",
"raw-loader": "^0.5.1",
"react-dev-utils": "^0.5.2",
@@ -44,6 +47,7 @@
"three-stl-loader": "^1.0.4",
"timeago.js": "^2.0.5",
"underscore": "^1.8.3",
+ "url-loader": "^0.5.8",
"visibilityjs": "^1.2.4",
"vue": "^2.2.6",
"vue-loader": "^11.3.4",
@@ -60,6 +64,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",
diff --git a/rubocop/cop/migration/add_column_with_default_to_large_table.rb b/rubocop/cop/migration/add_column_with_default_to_large_table.rb
new file mode 100644
index 00000000000..2372e6b60ea
--- /dev/null
+++ b/rubocop/cop/migration/add_column_with_default_to_large_table.rb
@@ -0,0 +1,51 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # This cop checks for `add_column_with_default` on a table that's been
+ # explicitly blacklisted because of its size.
+ #
+ # Even though this helper performs the update in batches to avoid
+ # downtime, using it with tables with millions of rows still causes a
+ # significant delay in the deploy process and is best avoided.
+ #
+ # See https://gitlab.com/gitlab-com/infrastructure/issues/1602 for more
+ # information.
+ class AddColumnWithDefaultToLargeTable < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = 'Using `add_column_with_default` on the `%s` table will take a ' \
+ 'long time to complete, and should be avoided unless absolutely ' \
+ 'necessary'.freeze
+
+ LARGE_TABLES = %i[
+ events
+ issues
+ merge_requests
+ namespaces
+ notes
+ projects
+ routes
+ users
+ ].freeze
+
+ def_node_matcher :add_column_with_default?, <<~PATTERN
+ (send nil :add_column_with_default $(sym ...) ...)
+ PATTERN
+
+ def on_send(node)
+ return unless in_migration?(node)
+
+ matched = add_column_with_default?(node)
+ return unless matched
+
+ table = matched.to_a.first
+ return unless LARGE_TABLES.include?(table)
+
+ add_offense(node, :expression, format(MSG, table))
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/add_column_with_default.rb b/rubocop/cop/migration/reversible_add_column_with_default.rb
index 54a920d4b49..f413f06f39b 100644
--- a/rubocop/cop/migration/add_column_with_default.rb
+++ b/rubocop/cop/migration/reversible_add_column_with_default.rb
@@ -5,29 +5,30 @@ module RuboCop
module Migration
# Cop that checks if `add_column_with_default` is used with `up`/`down` methods
# and not `change`.
- class AddColumnWithDefault < RuboCop::Cop::Cop
+ class ReversibleAddColumnWithDefault < RuboCop::Cop::Cop
include MigrationHelpers
+ def_node_matcher :add_column_with_default?, <<~PATTERN
+ (send nil :add_column_with_default $...)
+ PATTERN
+
+ def_node_matcher :defines_change?, <<~PATTERN
+ (def :change ...)
+ PATTERN
+
MSG = '`add_column_with_default` is not reversible so you must manually define ' \
'the `up` and `down` methods in your migration class, using `remove_column` in `down`'.freeze
def on_send(node)
return unless in_migration?(node)
-
- name = node.children[1]
-
- return unless name == :add_column_with_default
+ return unless add_column_with_default?(node)
node.each_ancestor(:def) do |def_node|
- next unless method_name(def_node) == :change
+ next unless defines_change?(def_node)
add_offense(def_node, :name)
end
end
-
- def method_name(node)
- node.children.first
- end
end
end
end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index d580aa6857a..4ff204f939e 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,9 +1,10 @@
require_relative 'cop/custom_error_class'
require_relative 'cop/gem_fetcher'
require_relative 'cop/migration/add_column'
-require_relative 'cop/migration/add_column_with_default'
+require_relative 'cop/migration/add_column_with_default_to_large_table'
require_relative 'cop/migration/add_concurrent_foreign_key'
require_relative 'cop/migration/add_concurrent_index'
require_relative 'cop/migration/add_index'
require_relative 'cop/migration/remove_concurrent_index'
require_relative 'cop/migration/remove_index'
+require_relative 'cop/migration/reversible_add_column_with_default'
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index 62236ed539a..54c1ef3dfdd 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -21,4 +21,3 @@ fi
echo "✔ Linting passed"
exit 0
-
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 6cacb81b8bc..c727a0e2d88 100755..100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -1,26 +1,46 @@
-#!/bin/sh
+. scripts/utils.sh
-retry() {
- if eval "$@"; then
- return 0
- fi
+export SETUP_DB=${SETUP_DB:-true}
+export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
+export BUNDLE_INSTALL_FLAGS="--without production --jobs $(nproc) --path vendor --retry 3 --quiet"
+
+# Determine the database by looking at the job name.
+# For example, we'll get pg if the job is `rspec pg 19 20`
+export GITLAB_DATABASE=$(echo $CI_JOB_NAME | cut -f2 -d' ')
+
+# This would make the default database postgresql, and we could also use
+# pg to mean postgresql.
+if [ "$GITLAB_DATABASE" != 'mysql' ]; then
+ export GITLAB_DATABASE='postgresql'
+fi
- for i in 2 1; do
- sleep 3s
- echo "Retrying $i..."
- if eval "$@"; then
- return 0
- fi
- done
- return 1
-}
-
-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.$GITLAB_DATABASE config/database.yml
+
+if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
+ sed -i 's/# host:.*/host: postgres/g' config/database.yml
+else # Assume it's mysql
+ sed -i 's/username:.*/username: root/g' config/database.yml
+ sed -i 's/password:.*/password:/g' config/database.yml
+ sed -i 's/# host:.*/host: mysql/g' config/database.yml
+fi
cp config/resque.yml.example config/resque.yml
sed -i 's/localhost/redis/g' config/resque.yml
-export FLAGS="--path vendor --retry 3 --quiet"
+cp config/gitlab.yml.example config/gitlab.yml
+
+if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
+ bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check
+fi
+
+# Only install knapsack after bundle install! Otherwise oddly some native
+# gems could not be found under some circumstance. No idea why, hours wasted.
+retry gem install knapsack fog-aws mime-types
+
+if [ "$SETUP_DB" != "false" ]; then
+ bundle exec rake db:drop db:create db:schema:load db:migrate
+
+ if [ "$GITLAB_DATABASE" = "mysql" ]; then
+ bundle exec rake add_limits_mysql
+ fi
+fi
diff --git a/scripts/static-analysis b/scripts/static-analysis
new file mode 100755
index 00000000000..192d9d4c3ba
--- /dev/null
+++ b/scripts/static-analysis
@@ -0,0 +1,40 @@
+#!/usr/bin/env ruby
+
+require ::File.expand_path('../lib/gitlab/popen', __dir__)
+
+tasks = [
+ %w[bundle exec rake config_lint],
+ %w[bundle exec rake flay],
+ %w[bundle exec rake haml_lint],
+ %w[bundle exec rake scss_lint],
+ %w[bundle exec rake brakeman],
+ %w[bundle exec license_finder],
+ %w[scripts/lint-doc.sh],
+ %w[yarn run eslint],
+ %w[bundle exec rubocop --require rubocop-rspec]
+]
+
+failed_tasks = tasks.reduce({}) do |failures, task|
+ output, status = Gitlab::Popen.popen(task)
+
+ puts "Running: #{task.join(' ')}"
+ puts output
+
+ failures[task.join(' ')] = output unless status.zero?
+
+ failures
+end
+
+if failed_tasks.empty?
+ puts 'All static analyses passed successfully.'
+else
+ puts "\n===================================================\n\n"
+ puts "Some static analyses failed:"
+
+ failed_tasks.each do |failed_task, output|
+ puts "\n**** #{failed_task} failed with the following error:\n\n"
+ puts output
+ end
+
+ exit 1
+end
diff --git a/scripts/utils.sh b/scripts/utils.sh
new file mode 100644
index 00000000000..6faa701f0ce
--- /dev/null
+++ b/scripts/utils.sh
@@ -0,0 +1,14 @@
+retry() {
+ if eval "$@"; then
+ return 0
+ fi
+
+ for i in 2 1; do
+ sleep 3s
+ echo "Retrying $i..."
+ if eval "$@"; then
+ return 0
+ fi
+ done
+ return 1
+}
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/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb
deleted file mode 100644
index 44e011fd3a8..00000000000
--- a/spec/controllers/blob_controller_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'spec_helper'
-
-describe Projects::BlobController do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
-
- project.team << [user, :master]
-
- allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz'])
- allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0'])
- controller.instance_variable_set(:@project, project)
- end
-
- describe "GET show" do
- render_views
-
- before do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- id: id)
- end
-
- context "valid branch, valid file" do
- let(:id) { 'master/README.md' }
- it { is_expected.to respond_with(:success) }
- end
-
- context "valid branch, invalid file" do
- let(:id) { 'master/invalid-path.rb' }
- it { is_expected.to respond_with(:not_found) }
- end
-
- context "invalid branch, valid file" do
- let(:id) { 'invalid-branch/README.md' }
- it { is_expected.to respond_with(:not_found) }
- end
-
- context "binary file" do
- let(:id) { 'binary-encoding/encoding/binary-1.bin' }
- it { is_expected.to respond_with(:success) }
- end
- end
-
- describe 'GET show with tree path' do
- render_views
-
- before do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- id: id)
- controller.instance_variable_set(:@blob, nil)
- end
-
- context 'redirect to tree' do
- let(:id) { 'markdown/doc' }
- it 'redirects' do
- expect(subject).
- to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc")
- end
- end
- end
-end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 6075259ea99..762e90f4a16 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Dashboard::TodosController do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:empty_project) }
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
new file mode 100644
index 00000000000..d321bfcea9d
--- /dev/null
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Oauth::AuthorizationsController do
+ let(:user) { create(:user) }
+
+ let(:doorkeeper) do
+ Doorkeeper::Application.create(
+ name: "MyApp",
+ redirect_uri: 'http://example.com',
+ scopes: "")
+ end
+
+ let(:params) do
+ {
+ response_type: "code",
+ client_id: doorkeeper.uid,
+ redirect_uri: doorkeeper.redirect_uri,
+ state: 'state'
+ }
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #new' do
+ context 'without valid params' do
+ it 'returns 200 code and renders error view' do
+ get :new
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('doorkeeper/authorizations/error')
+ end
+ end
+
+ context 'with valid params' do
+ it 'returns 200 code and renders view' do
+ get :new, params
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('doorkeeper/authorizations/new')
+ end
+
+ it 'deletes session.user_return_to and redirects when skip authorization' do
+ request.session['user_return_to'] = 'http://example.com'
+ allow(controller).to receive(:skip_authorization?).and_return(true)
+
+ get :new, params
+
+ expect(request.session['user_return_to']).to be_nil
+ expect(response).to have_http_status(302)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
index 98a43e278b2..98a43e278b2 100644
--- a/spec/controllers/profiles/personal_access_tokens_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 3e9f272a0d8..3b3caa9d3e6 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -3,6 +3,57 @@ require 'rails_helper'
describe Projects::BlobController do
let(:project) { create(:project, :public, :repository) }
+ describe "GET show" do
+ render_views
+
+ context 'with file path' do
+ before do
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id)
+ end
+
+ context "valid branch, valid file" do
+ let(:id) { 'master/README.md' }
+ it { is_expected.to respond_with(:success) }
+ end
+
+ context "valid branch, invalid file" do
+ let(:id) { 'master/invalid-path.rb' }
+ it { is_expected.to respond_with(:not_found) }
+ end
+
+ context "invalid branch, valid file" do
+ let(:id) { 'invalid-branch/README.md' }
+ it { is_expected.to respond_with(:not_found) }
+ end
+
+ context "binary file" do
+ let(:id) { 'binary-encoding/encoding/binary-1.bin' }
+ it { is_expected.to respond_with(:success) }
+ end
+ end
+
+ context 'with tree path' do
+ before do
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id)
+ controller.instance_variable_set(:@blob, nil)
+ end
+
+ context 'redirect to tree' do
+ let(:id) { 'markdown/doc' }
+ it 'redirects' do
+ expect(subject).
+ to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc")
+ end
+ end
+ end
+ end
+
describe 'GET diff' do
let(:user) { create(:user) }
@@ -106,7 +157,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 +229,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..22193eac672 100644
--- a/spec/controllers/projects/builds_controller_spec.rb
+++ b/spec/controllers/projects/builds_controller_spec.rb
@@ -3,14 +3,67 @@ require 'spec_helper'
describe Projects::BuildsController do
include ApiHelpers
- let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) }
-
- before do
- sign_in(user)
- end
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:user) { create(:user) }
describe 'GET index' do
+ context 'when scope is pending' do
+ before do
+ create(:ci_build, :pending, pipeline: pipeline)
+
+ get_index(scope: 'pending')
+ end
+
+ it 'has only pending builds' do
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:builds).first.status).to eq('pending')
+ end
+ end
+
+ context 'when scope is running' do
+ before do
+ create(:ci_build, :running, pipeline: pipeline)
+
+ get_index(scope: 'running')
+ end
+
+ it 'has only running builds' do
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:builds).first.status).to eq('running')
+ end
+ end
+
+ context 'when scope is finished' do
+ before do
+ create(:ci_build, :success, pipeline: pipeline)
+
+ get_index(scope: 'finished')
+ end
+
+ it 'has only finished builds' do
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:builds).first.status).to eq('success')
+ end
+ end
+
+ context 'when page is specified' do
+ let(:last_page) { project.builds.page.total_pages }
+
+ context 'when page number is eligible' do
+ before do
+ create_list(:ci_build, 2, pipeline: pipeline)
+
+ get_index(page: last_page.to_param)
+ end
+
+ it 'redirects to the page' do
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:builds).current_page).to eq(last_page)
+ end
+ end
+ end
+
context 'number of queries' do
before do
Ci::Build::AVAILABLE_STATUSES.each do |status|
@@ -25,13 +78,8 @@ describe Projects::BuildsController do
RequestStore.clear!
end
- def render
- get :index, namespace_id: project.namespace,
- project_id: project
- end
-
it "verifies number of queries" do
- recorded = ActiveRecord::QueryRecorder.new { render }
+ recorded = ActiveRecord::QueryRecorder.new { get_index }
expect(recorded.count).to be_within(5).of(8)
end
@@ -41,10 +89,83 @@ describe Projects::BuildsController do
pipeline: pipeline, name: name, status: status)
end
end
+
+ def get_index(**extra_params)
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ get :index, params.merge(extra_params)
+ end
+ end
+
+ describe 'GET show' do
+ context 'when build exists' do
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+ before do
+ get_show(id: build.id)
+ end
+
+ it 'has a build' do
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:build).id).to eq(build.id)
+ end
+ end
+
+ context 'when build does not exist' do
+ before do
+ get_show(id: 1234)
+ end
+
+ it 'renders not_found' do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ def get_show(**extra_params)
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ get :show, params.merge(extra_params)
+ end
+ end
+
+ describe 'GET trace.json' do
+ before do
+ get_trace
+ end
+
+ context 'when build has a trace' do
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
+
+ it 'returns a trace' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['html']).to eq('BUILD TRACE')
+ end
+ end
+
+ context 'when build has no traces' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'returns no traces' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['html']).to be_nil
+ end
+ end
+
+ def get_trace
+ get :trace, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id,
+ format: :json
+ end
end
describe 'GET status.json' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:status) { build.detailed_status(double('user')) }
@@ -60,7 +181,266 @@ 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
+
+ describe 'GET trace.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:user) { create(:user) }
+
+ context 'when user is logged in as developer' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ get_trace
+ end
+
+ it 'traces build log' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['id']).to eq build.id
+ expect(json_response['status']).to eq build.status
+ end
+ end
+
+ context 'when user is logged in as non member' do
+ before do
+ sign_in(user)
+
+ get_trace
+ end
+
+ it 'traces build log' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['id']).to eq build.id
+ expect(json_response['status']).to eq build.status
+ end
+ end
+
+ def get_trace
+ get :trace, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id,
+ format: :json
+ end
+ end
+
+ describe 'POST retry' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ post_retry
+ end
+
+ context 'when build is retryable' do
+ let(:build) { create(:ci_build, :retryable, pipeline: pipeline) }
+
+ it 'redirects to the retried build page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_build_path(id: Ci::Build.last.id))
+ end
+ end
+
+ context 'when build is not retryable' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'renders unprocessable_entity' do
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+
+ def post_retry
+ post :retry, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
+ end
+ end
+
+ describe 'POST play' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ post_play
+ end
+
+ context 'when build is playable' do
+ let(:build) { create(:ci_build, :playable, pipeline: pipeline) }
+
+ it 'redirects to the played build page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_build_path(id: build.id))
+ end
+
+ it 'transits to pending' do
+ expect(build.reload).to be_pending
+ end
+ end
+
+ context 'when build is not playable' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'renders unprocessable_entity' do
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+
+ def post_play
+ post :play, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
+ end
+ end
+
+ describe 'POST cancel' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ post_cancel
+ end
+
+ context 'when build is cancelable' do
+ let(:build) { create(:ci_build, :cancelable, pipeline: pipeline) }
+
+ it 'redirects to the canceled build page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_build_path(id: build.id))
+ end
+
+ it 'transits to canceled' do
+ expect(build.reload).to be_canceled
+ end
+ end
+
+ context 'when build is not cancelable' do
+ let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
+
+ it 'returns unprocessable_entity' do
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+
+ def post_cancel
+ post :cancel, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
+ end
+ end
+
+ describe 'POST cancel_all' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ context 'when builds are cancelable' do
+ before do
+ create_list(:ci_build, 2, :cancelable, pipeline: pipeline)
+
+ post_cancel_all
+ end
+
+ it 'redirects to a index page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_builds_path)
+ end
+
+ it 'transits to canceled' do
+ expect(Ci::Build.all).to all(be_canceled)
+ end
+ end
+
+ context 'when builds are not cancelable' do
+ before do
+ create_list(:ci_build, 2, :canceled, pipeline: pipeline)
+
+ post_cancel_all
+ end
+
+ it 'redirects to a index page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_builds_path)
+ end
+ end
+
+ def post_cancel_all
+ post :cancel_all, namespace_id: project.namespace,
+ project_id: project
+ end
+ end
+
+ describe 'POST erase' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ post_erase
+ end
+
+ context 'when build is erasable' do
+ let(:build) { create(:ci_build, :erasable, :trace, pipeline: pipeline) }
+
+ it 'redirects to the erased build page' do
+ expect(response).to have_http_status(:found)
+ expect(response).to redirect_to(namespace_project_build_path(id: build.id))
+ end
+
+ it 'erases artifacts' do
+ expect(build.artifacts_file.exists?).to be_falsey
+ expect(build.artifacts_metadata.exists?).to be_falsey
+ end
+
+ it 'erases trace' do
+ expect(build.trace.exist?).to be_falsey
+ end
+ end
+
+ context 'when build is not erasable' do
+ let(:build) { create(:ci_build, :erased, pipeline: pipeline) }
+
+ it 'returns unprocessable_entity' do
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+
+ def post_erase
+ post :erase, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
+ end
+ end
+
+ describe 'GET raw' do
+ before do
+ get_raw
+ end
+
+ context 'when build has a trace file' do
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
+
+ it 'send a trace file' do
+ expect(response).to have_http_status(:ok)
+ expect(response.content_type).to eq 'text/plain; charset=utf-8'
+ expect(response.body).to eq 'BUILD TRACE'
+ end
+ end
+
+ context 'when build does not have a trace file' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'returns not_found' do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ def get_raw
+ post :raw, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
end
end
end
diff --git a/spec/controllers/projects/builds_controller_specs.rb b/spec/controllers/projects/builds_controller_specs.rb
deleted file mode 100644
index d501f7b3155..00000000000
--- a/spec/controllers/projects/builds_controller_specs.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'spec_helper'
-
-describe Projects::BuildsController do
- include ApiHelpers
-
- let(:project) { create(:empty_project, :public) }
-
- describe 'GET trace.json' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:user) { create(:user) }
-
- context 'when user is logged in as developer' do
- before do
- project.add_developer(user)
- sign_in(user)
- get_trace
- end
-
- it 'traces build log' do
- expect(response).to have_http_status(:ok)
- expect(json_response['id']).to eq build.id
- expect(json_response['status']).to eq build.status
- end
- end
-
- context 'when user is logged in as non member' do
- before do
- sign_in(user)
- get_trace
- end
-
- it 'traces build log' do
- expect(response).to have_http_status(:ok)
- expect(json_response['id']).to eq build.id
- expect(json_response['status']).to eq build.status
- end
- end
-
- def get_trace
- get :trace, namespace_id: project.namespace,
- project_id: project,
- id: build.id,
- format: :json
- end
- end
-end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 5525fbd8130..5c478534ff3 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Projects::EnvironmentsController do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
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..a793da4162a 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Projects::MergeRequestsController do
- include ApiHelpers
-
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -1208,7 +1206,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/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 14207bf6b7a..47e61c3cea8 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -5,6 +5,7 @@ describe Projects::MilestonesController do
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
let(:issue) { create(:issue, project: project, milestone: milestone) }
+ let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
before do
@@ -13,6 +14,20 @@ describe Projects::MilestonesController do
controller.instance_variable_set(:@project, project)
end
+ describe "#show" do
+ render_views
+
+ def view_milestone
+ get :show, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
+ end
+
+ it 'shows milestone page' do
+ view_milestone
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
describe "#destroy" do
it "removes milestone" do
expect(issue.milestone_id).to eq(milestone.id)
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index d8f9bfd0d37..b9bacc5a64a 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Projects::PipelinesController do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) }
@@ -86,7 +84,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/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 16365642a34..2d892f4a2b7 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -8,6 +8,7 @@ describe Projects::ServicesController do
before do
sign_in(user)
project.team << [user, :master]
+
controller.instance_variable_set(:@project, project)
controller.instance_variable_set(:@service, service)
end
@@ -18,20 +19,60 @@ describe Projects::ServicesController do
end
describe "#test" do
+ context 'when can_test? returns false' do
+ it 'renders 404' do
+ allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
+
+ get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
context 'success' do
+ context 'with empty project' do
+ let(:project) { create(:empty_project) }
+
+ context 'with chat notification service' do
+ let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
+
+ it 'redirects and show success message' do
+ allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
+
+ get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+
+ expect(response).to redirect_to(root_path)
+ expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ end
+ end
+
+ it 'redirects and show success message' do
+ expect(service).to receive(:test).and_return(success: true, result: 'done')
+
+ get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+
+ expect(response).to redirect_to(root_path)
+ expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ end
+ end
+
it "redirects and show success message" do
- expect(service).to receive(:test).and_return({ success: true, result: 'done' })
+ expect(service).to receive(:test).and_return(success: true, result: 'done')
+
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
- expect(response.status).to redirect_to('/')
+
+ expect(response).to redirect_to(root_path)
expect(flash[:notice]).to eq('We sent a request to the provided URL')
end
end
context 'failure' do
it "redirects and show failure message" do
- expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' })
+ expect(service).to receive(:test).and_return(success: false, result: 'Bad test')
+
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
- expect(response.status).to redirect_to('/')
+
+ expect(response).to redirect_to(root_path)
expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test')
end
end
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb
index 9a7beeff6fe..c5a4153d991 100644
--- a/spec/controllers/projects/todo_controller_spec.rb
+++ b/spec/controllers/projects/todos_controller_spec.rb
@@ -1,8 +1,6 @@
require('spec_helper')
describe Projects::TodosController do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
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/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb
new file mode 100644
index 00000000000..92addf30307
--- /dev/null
+++ b/spec/controllers/projects/wikis_controller_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Projects::WikisController do
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:user) { create(:user) }
+
+ describe 'POST #preview_markdown' do
+ it 'renders json in a correct format' do
+ sign_in(user)
+
+ post :preview_markdown, namespace_id: project.namespace, project_id: project, id: 'page/path', text: '*Markdown* text'
+
+ expect(JSON.parse(response.body).keys).to match_array(%w(body references))
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index a88ffc1ea6a..eafc2154568 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -398,4 +398,14 @@ describe ProjectsController do
expect(parsed_body["Commits"]).to include("123456")
end
end
+
+ describe 'POST #preview_markdown' do
+ it 'renders json in a correct format' do
+ sign_in(user)
+
+ post :preview_markdown, namespace_id: public_project.namespace, id: public_project, text: '*Markdown* text'
+
+ expect(JSON.parse(response.body).keys).to match_array(%w(body references))
+ 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/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 5de3b9890ef..234f3edd3d8 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -521,4 +521,16 @@ describe SnippetsController do
end
end
end
+
+ describe 'POST #preview_markdown' do
+ let(:snippet) { create(:personal_snippet, :public) }
+
+ it 'renders json in a correct format' do
+ sign_in(user)
+
+ post :preview_markdown, id: snippet, text: '*Markdown* text'
+
+ expect(JSON.parse(response.body).keys).to match_array(%w(body references))
+ end
+ end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index b62def83ee4..78ddd8d5584 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -79,6 +79,19 @@ FactoryGirl.define do
manual
end
+ trait :retryable do
+ success
+ end
+
+ trait :cancelable do
+ pending
+ end
+
+ trait :erasable do
+ success
+ artifacts
+ end
+
trait :tags do
tag_list [:docker, :ruby]
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 0b6977e3b17..f1fd1fd7f73 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -8,6 +8,10 @@ FactoryGirl.define do
confidential true
end
+ trait :opened do
+ state :opened
+ end
+
trait :closed do
state :closed
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/merge_requests_closing_issues.rb b/spec/factories/merge_requests_closing_issues.rb
new file mode 100644
index 00000000000..fdbdc00cad7
--- /dev/null
+++ b/spec/factories/merge_requests_closing_issues.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :merge_requests_closing_issues do
+ issue
+ merge_request
+ end
+end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 39c2a9dd1fb..0210e871a63 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -1,6 +1,7 @@
FactoryGirl.define do
factory :project_hook do
url { generate(:url) }
+ enable_ssl_verification false
trait :token do
token { SecureRandom.hex(10) }
@@ -11,6 +12,7 @@ FactoryGirl.define do
merge_requests_events true
tag_push_events true
issues_events true
+ confidential_issues_events true
note_events true
build_events true
pipeline_events true
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 0db2fe04edd..3580752a805 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -32,6 +32,10 @@ FactoryGirl.define do
request_access_enabled true
end
+ trait :with_avatar do
+ avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
+ end
+
trait :repository do
# no-op... for now!
end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 88f6c265505..62aa71ae8d8 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -1,6 +1,19 @@
FactoryGirl.define do
factory :service do
project factory: :empty_project
+ type 'Service'
+ end
+
+ factory :custom_issue_tracker_service, class: CustomIssueTrackerService do
+ project factory: :empty_project
+ type 'CustomIssueTrackerService'
+ category 'issue_tracker'
+ active true
+ properties(
+ project_url: 'https://project.url.com',
+ issues_url: 'https://issues.url.com',
+ new_issue_url: 'https://newissue.url.com'
+ )
end
factory :kubernetes_service do
diff --git a/spec/features/admin/admin_cohorts_spec.rb b/spec/features/admin/admin_cohorts_spec.rb
new file mode 100644
index 00000000000..dd14ffdb2ce
--- /dev/null
+++ b/spec/features/admin/admin_cohorts_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+feature 'Admin cohorts page', feature: true do
+ before do
+ login_as :admin
+ end
+
+ scenario 'See users count per month' do
+ 2.times { create(:user) }
+
+ visit admin_cohorts_path
+
+ expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
+ end
+end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index fb519a9bf12..c5f24d412d7 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Admin::Hooks", feature: true do
+describe 'Admin::Hooks', feature: true do
before do
@project = create(:project)
login_as :admin
@@ -8,24 +8,24 @@ describe "Admin::Hooks", feature: true do
@system_hook = create(:system_hook)
end
- describe "GET /admin/hooks" do
- it "is ok" do
+ describe 'GET /admin/hooks' do
+ it 'is ok' do
visit admin_root_path
- page.within ".layout-nav" do
- click_on "Hooks"
+ page.within '.layout-nav' do
+ click_on 'Hooks'
end
expect(current_path).to eq(admin_hooks_path)
end
- it "has hooks list" do
+ it 'has hooks list' do
visit admin_hooks_path
expect(page).to have_content(@system_hook.url)
end
end
- describe "New Hook" do
+ describe 'New Hook' do
let(:url) { generate(:url) }
it 'adds new hook' do
@@ -40,11 +40,36 @@ describe "Admin::Hooks", feature: true do
end
end
- describe "Test" do
+ describe 'Update existing hook' do
+ let(:new_url) { generate(:url) }
+
+ it 'updates existing hook' do
+ visit admin_hooks_path
+
+ click_link 'Edit'
+ fill_in 'hook_url', with: new_url
+ check 'Enable SSL verification'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'SSL Verification: enabled'
+ expect(current_path).to eq(admin_hooks_path)
+ expect(page).to have_content(new_url)
+ end
+ end
+
+ describe 'Remove existing hook' do
+ it 'remove existing hook' do
+ visit admin_hooks_path
+
+ expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ end
+ end
+
+ describe 'Test' do
before do
WebMock.stub_request(:post, @system_hook.url)
visit admin_hooks_path
- click_link "Test hook"
+ click_link 'Test hook'
end
it { expect(current_path).to eq(admin_hooks_path) }
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index 6d6c9165c83..fa3d9ee25c0 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
RSpec.describe 'admin issues labels' do
- include WaitForAjax
-
let!(:bug_label) { Label.create(title: 'bug', template: true) }
let!(:feature_label) { Label.create(title: 'feature', template: true) }
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/admin/admin_requests_profiles_spec.rb b/spec/features/admin/admin_requests_profiles_spec.rb
new file mode 100644
index 00000000000..e8ecb70306b
--- /dev/null
+++ b/spec/features/admin/admin_requests_profiles_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe 'Admin::RequestsProfilesController', feature: true do
+ before do
+ FileUtils.mkdir_p(Gitlab::RequestProfiler::PROFILES_DIR)
+ login_as(:admin)
+ end
+
+ after do
+ Gitlab::RequestProfiler.remove_all_profiles
+ end
+
+ describe 'GET /admin/requests_profiles' do
+ it 'shows the current profile token' do
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+
+ visit admin_requests_profiles_path
+
+ expect(page).to have_content("X-Profile-Token: #{Gitlab::RequestProfiler.profile_token}")
+ end
+
+ it 'lists all available profiles' do
+ time1 = 1.hour.ago
+ time2 = 2.hours.ago
+ time3 = 3.hours.ago
+ profile1 = "|gitlab-org|gitlab-ce_#{time1.to_i}.html"
+ profile2 = "|gitlab-org|gitlab-ce_#{time2.to_i}.html"
+ profile3 = "|gitlab-com|infrastructure_#{time3.to_i}.html"
+
+ FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile1}")
+ FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile2}")
+ FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile3}")
+
+ visit admin_requests_profiles_path
+
+ within('.panel', text: '/gitlab-org/gitlab-ce') do
+ expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile1)}']", text: time1.to_s(:long))
+ expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile2)}']", text: time2.to_s(:long))
+ end
+
+ within('.panel', text: '/gitlab-com/infrastructure') do
+ expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile3)}']", text: time3.to_s(:long))
+ end
+ end
+ end
+
+ describe 'GET /admin/requests_profiles/:profile' do
+ context 'when a profile exists' do
+ it 'displays the content of the profile' do
+ content = 'This is a request profile'
+ profile = "|gitlab-org|gitlab-ce_#{Time.now.to_i}.html"
+
+ File.write("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile}", content)
+
+ visit admin_requests_profile_path(profile)
+
+ expect(page).to have_content(content)
+ end
+ end
+
+ context 'when a profile does not exist' do
+ it 'shows an error message' do
+ visit admin_requests_profile_path('|non|existent_12345.html')
+
+ expect(page).to have_content('Profile not found')
+ end
+ end
+ end
+end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index f6c3bc6a58d..c5b1ef1295c 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe "Admin::Users", feature: true do
- include WaitForAjax
-
let!(:user) do
create(:omniauth_user, provider: 'twitter', extern_uid: '123456')
end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 55e10a1a89b..7a2987e815d 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -53,7 +53,7 @@ describe "User Feed", feature: true do
end
it 'has XHTML summaries in issue descriptions' do
- expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p dir="auto">I guess/
+ expect(body).to match /<hr ?\/>/
end
it 'has XHTML summaries in notes' do
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
index 6f36c74c911..67b0f006854 100644
--- a/spec/features/auto_deploy_spec.rb
+++ b/spec/features/auto_deploy_spec.rb
@@ -1,10 +1,8 @@
require 'spec_helper'
describe 'Auto deploy' do
- include WaitForAjax
-
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
project.create_kubernetes_service(
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index 248c31115ad..505e0b5c355 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Issue Boards add issue modal', :feature, :js do
- include WaitForAjax
include WaitForVueResource
let(:project) { create(:empty_project, :public) }
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 30ad169e30e..a172ce1e8c0 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do
- include WaitForAjax
include WaitForVueResource
include DragTo
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index e6d7cf106d4..f04a1a89e96 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Issue Boards new issue', feature: true, js: true do
- include WaitForAjax
include WaitForVueResource
let(:project) { create(:empty_project, :public) }
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 3332e07ec31..bafa4f05937 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do
- include WaitForAjax
include WaitForVueResource
let(:user) { create(:user) }
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 35d090c4b7f..496faf87a16 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -1,10 +1,8 @@
require 'spec_helper'
feature 'Contributions Calendar', :feature, :js do
- include WaitForAjax
-
let(:user) { create(:user) }
- let(:contributed_project) { create(:project, :public) }
+ let(:contributed_project) { create(:empty_project, :public) }
let(:issue_note) { create(:note, project: contributed_project) }
# Ex/ Sunday Jan 1, 2016
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 881f1fca4d1..e6c4ab24de5 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'Commits' do
include CiStatusHelper
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
describe 'CI' do
before do
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index 55df7e45f79..f197fb44608 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'Copy as GFM', feature: true, js: true do
- include GitlabMarkdownHelper
+ include MarkupHelper
include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
@@ -433,7 +433,7 @@ describe 'Copy as GFM', feature: true, js: true do
end
describe 'Copying code' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
context 'from a diff' do
before do
@@ -479,6 +479,7 @@ describe 'Copy as GFM', feature: true, js: true do
context 'from a blob' do
before do
visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb'))
+ wait_for_ajax
end
context 'selecting one word of text' do
@@ -520,6 +521,7 @@ describe 'Copy as GFM', feature: true, js: true do
context 'from a GFM code block' do
before do
visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md'))
+ wait_for_ajax
end
context 'selecting one word of text' do
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 0648c89a5c7..b93275c330b 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -1,11 +1,9 @@
require 'spec_helper'
feature 'Cycle Analytics', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:guest) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(issue) }
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index dc9d09fa396..0e9e3f78be2 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Tooltips on .timeago dates', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:created_date) { Date.yesterday.to_time }
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index ca04107d33a..52b4d82e856 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'Dashboard Groups page', js: true, feature: true do
- include WaitForAjax
-
let!(:user) { create :user }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, :nested) }
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index 49d93db58a9..16c214ae060 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Project member activity', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public, name: 'x', namespace: user.namespace) }
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index 8c61cdebc4b..b6b87905231 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe "Dashboard Issues filtering", feature: true, js: true do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:milestone) { create(:milestone, project: project) }
context 'filtering by milestone' do
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 8c64b050e19..76c77e0bc5f 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -1,10 +1,8 @@
require 'spec_helper'
feature 'Expand and collapse diffs', js: true, feature: true do
- include WaitForAjax
-
let(:branch) { 'expand-collapse-diffs' }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
login_as :admin
diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb
index 9daaaa8e555..9828cb179a7 100644
--- a/spec/features/explore/groups_list_spec.rb
+++ b/spec/features/explore/groups_list_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe 'Explore Groups page', js: true, feature: true do
- include WaitForAjax
-
+describe 'Explore Groups page', :js, :feature do
let!(:user) { create :user }
let!(:group) { create(:group) }
let!(:public_group) { create(:group, :public) }
@@ -48,19 +46,39 @@ describe 'Explore Groups page', js: true, feature: true do
it 'shows non-archived projects count' do
# Initially project is not archived
expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
-
+
# Archive project
empty_project.archive!
visit explore_groups_path
# Check project count
expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("0")
-
+
# Unarchive project
empty_project.unarchive!
visit explore_groups_path
# Check project count
- expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
+ expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
+ end
+
+ describe 'landing component' do
+ it 'should show a landing component' do
+ expect(page).to have_content('Below you will find all the groups that are public.')
+ end
+
+ it 'should be dismissable' do
+ find('.dismiss-button').click
+
+ expect(page).not_to have_content('Below you will find all the groups that are public.')
+ end
+
+ it 'should persistently not show once dismissed' do
+ find('.dismiss-button').click
+
+ visit explore_groups_path
+
+ expect(page).not_to have_content('Below you will find all the groups that are public.')
+ end
end
end
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index 876f33dd03e..01b1aee4fd3 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -1,28 +1,28 @@
require 'spec_helper'
describe "GitLab Flavored Markdown", feature: true do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:fred) do
- u = create(:user, name: "fred")
- project.team << [u, :master]
- u
+ create(:user, name: 'fred') do |user|
+ project.add_master(user)
+ end
end
before do
- allow_any_instance_of(Commit).to receive(:title).
- and_return("fix #{issue.to_reference}\n\nask #{fred.to_reference} for details")
+ login_as(:user)
+ project.add_developer(@user)
end
- let(:commit) { project.commit }
+ describe "for commits" do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit }
- before do
- login_as :user
- project.team << [@user, :developer]
- end
+ before do
+ allow_any_instance_of(Commit).to receive(:title).
+ and_return("fix #{issue.to_reference}\n\nask #{fred.to_reference} for details")
+ end
- describe "for commits" do
it "renders title in commits#index" do
visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1)
@@ -92,6 +92,8 @@ describe "GitLab Flavored Markdown", feature: true do
end
describe "for merge requests" do
+ let(:project) { create(:project, :repository) }
+
before do
@merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix #{issue.to_reference}")
end
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index f6409e00f22..4b22b07494d 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
feature 'Global search', feature: true do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
before do
project.team << [user, :master]
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 1b3747c390b..45f57845c74 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -23,4 +23,20 @@ feature 'Group issues page', feature: true do
it_behaves_like "an autodiscoverable RSS feed without a private token"
end
end
+
+ context 'assignee', :js do
+ let(:access_level) { ProjectFeature::ENABLED }
+ let(:user) { user_in_group }
+ let(:user2) { user_outside_group }
+ let(:path) { issues_group_path(group) }
+
+ it 'filters by only group users' do
+ click_button('Assignee')
+
+ wait_for_ajax
+
+ expect(find('.dropdown-menu-assignee')).to have_link(user.name)
+ expect(find('.dropdown-menu-assignee')).not_to have_link(user2.name)
+ end
+ end
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/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index 3dc872ae520..f3ec80bb149 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -68,7 +68,7 @@ describe 'issuable list', feature: true do
source_project: project,
source_branch: generate(:branch))
- MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request)
+ create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
end
end
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 8e67ab028d7..71df3c949db 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Awards Emoji', feature: true do
- include WaitForAjax
include WaitForVueResource
let!(:project) { create(:project, :public) }
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index 2f59630b4fb..1de50d6d77e 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Issues > Labels bulk assignment', feature: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let!(:project) { create(:project) }
let!(:issue1) { create(:issue, project: project, title: "Issue 1") }
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
index 88e2cc60d79..3a5a79e03f4 100644
--- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
@@ -4,7 +4,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue', fe
let(:user) { create(:user) }
let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
let(:merge_request) { create(:merge_request, source_project: project) }
- let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first }
+ let!(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
describe 'As a user with access to the project' do
before do
@@ -74,8 +74,8 @@ feature 'Resolve an open discussion in a merge request by creating an issue', fe
it 'Shows a notice to ask someone else to resolve the discussions' do
expect(page).to have_content("The discussion at #{merge_request.to_reference}"\
- "(discussion #{discussion.first_note.id}) will stay unresolved."\
- "Ask someone with permission to resolve it.")
+ " (discussion #{discussion.first_note.id}) will stay unresolved."\
+ " Ask someone with permission to resolve it.")
end
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 3d1a9ed1722..0b573d7cef4 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
describe 'Dropdown assignee', :feature, :js do
include FilteredSearchHelpers
- include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 990e3b3e60c..0579d6c80ab 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
describe 'Dropdown author', js: true, feature: true do
include FilteredSearchHelpers
- include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index cae01f37b6b..b9a37cfcc22 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
describe 'Dropdown hint', :js, :feature do
include FilteredSearchHelpers
- include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 6f00066de4d..c824aa6a414 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -3,7 +3,6 @@ require 'spec_helper'
describe 'Filter issues', js: true, feature: true do
include Devise::Test::IntegrationHelpers
include FilteredSearchHelpers
- include WaitForAjax
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
@@ -13,7 +12,7 @@ describe 'Filter issues', js: true, feature: true do
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
let!(:bug_label) { create(:label, project: project, title: 'bug') }
- let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') }
+ let!(:caps_sensitive_label) { create(:label, project: project, title: 'CaPs') }
let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index f506065a242..08fe3b4553b 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe 'Recent searches', js: true, feature: true do
include FilteredSearchHelpers
- include WaitForAjax
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 28137f11b92..3ea95aed0a6 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
describe 'Search bar', js: true, feature: true do
include FilteredSearchHelpers
- include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 7135565294b..ad29911248f 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
feature 'GFM autocomplete', feature: true, js: true do
- include WaitForAjax
let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') }
@@ -46,6 +45,33 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type')
end
+ it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do
+ note = find('#note_note')
+
+ # Number.
+ page.within '.timeline-content-form' do
+ note.native.send_keys('7:')
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+
+ # ASCII letter.
+ page.within '.timeline-content-form' do
+ note.set('')
+ note.native.send_keys('w:')
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+
+ # Non-ASCII letter.
+ page.within '.timeline-content-form' do
+ note.set('')
+ note.native.send_keys('Ё:')
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
+
it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do
find('#note_note').native.send_keys('')
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 7b9d4534ada..baacd7edb86 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
feature 'Issue Sidebar', feature: true do
- include WaitForAjax
include MobileHelpers
let(:project) { create(:project, :public) }
@@ -120,6 +119,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/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index ae5da3877a8..7fa83c1fcf7 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Multiple issue updating from issues#index', feature: true do
- include WaitForAjax
-
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 0a9cd11ad6e..4cd6c1171ac 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
feature 'Issues > User uses slash commands', feature: true, js: true do
include SlashCommandsHelpers
- include WaitForAjax
it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do
let(:issuable) { create(:issue, project: project) }
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 362d167befa..81cc8513454 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -4,22 +4,14 @@ describe 'Issues', feature: true do
include DropzoneHelper
include IssueHelpers
include SortingHelper
- include WaitForAjax
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
before do
login_as :user
user2 = create(:user)
project.team << [[@user, user2], :developer]
-
- project.repository.create_file(
- @user,
- '.gitlab/issue_templates/bug.md',
- 'this is a test "bug" template',
- message: 'added issue template',
- branch_name: 'master')
end
describe 'Edit issue' do
@@ -378,7 +370,7 @@ describe 'Issues', feature: true do
end
describe 'when I want to reset my incoming email token' do
- let(:project1) { create(:project, namespace: @user.namespace) }
+ let(:project1) { create(:empty_project, namespace: @user.namespace) }
let!(:issue) { create(:issue, project: project1) }
before do
@@ -414,7 +406,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
@@ -609,7 +602,16 @@ describe 'Issues', feature: true do
end
context 'form filled by URL parameters' do
+ let(:project) { create(:project, :public, :repository) }
+
before do
+ project.repository.create_file(
+ @user,
+ '.gitlab/issue_templates/bug.md',
+ 'this is a test "bug" template',
+ message: 'added issue template',
+ branch_name: 'master')
+
visit new_namespace_project_issue_path(project.namespace, project, issuable_template: 'bug')
end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 894df13a2dc..ba930de937d 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -26,7 +26,7 @@ require 'erb'
describe 'GitLab Markdown', feature: true do
include Capybara::Node::Matchers
- include GitlabMarkdownHelper
+ include MarkupHelper
include MarkdownMatchers
# Sometimes it can be useful to see the parsed output of the Markdown document
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 18508a44184..43977ad2fc5 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Merge request conflict resolution', js: true, feature: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index 3a4ec07b2b0..f0fec625108 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -20,13 +20,14 @@ 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
+ first('.dropdown-source-branch .dropdown-content')
+ 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'
@@ -34,6 +35,7 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Target branch')
first('.js-target-branch').click
+ first('.dropdown-target-branch .dropdown-content')
first('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0').click
expect(page).to have_content "b83d6e3"
@@ -46,8 +48,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/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb
index 0952b17b63e..648678e2b1a 100644
--- a/spec/features/merge_requests/deleted_source_branch_spec.rb
+++ b/spec/features/merge_requests/deleted_source_branch_spec.rb
@@ -4,8 +4,6 @@ require 'spec_helper'
# message to be shown by JavaScript when the source branch was deleted.
# Please do not remove "js: true".
describe 'Deleted source branch', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index 218d95a88b8..b2e170513c4 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Diff note avatars', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index 4a6c76a5caf..7dee3b852ca 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -1,11 +1,8 @@
require 'spec_helper'
feature 'Diffs URL', js: true, feature: true do
- before do
- login_as :admin
- @merge_request = create(:merge_request)
- @project = @merge_request.source_project
- end
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
context 'when visit with */* as accept header' do
before(:each) do
@@ -13,9 +10,9 @@ feature 'Diffs URL', js: true, feature: true do
end
it 'renders the notes' do
- create :note_on_merge_request, project: @project, noteable: @merge_request, note: 'Rebasing with master'
+ create :note_on_merge_request, project: project, noteable: merge_request, note: 'Rebasing with master'
- visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
# Load notes and diff through AJAX
expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master')
@@ -25,10 +22,9 @@ feature 'Diffs URL', js: true, feature: true do
context 'when merge request has overflow' do
it 'displays warning' do
- allow_any_instance_of(MergeRequestDiff).to receive(:overflow?).and_return(true)
- allow(Commit).to receive(:max_diff_options).and_return(max_files: 20, max_lines: 20)
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 3)
- visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
page.within('.alert') do
expect(page).to have_text("Too many changes to show. Plain diff Email patch To preserve
@@ -36,4 +32,35 @@ feature 'Diffs URL', js: true, feature: true do
end
end
end
+
+ context 'when editing file' do
+ let(:author_user) { create(:user) }
+ let(:user) { create(:user) }
+ let(:forked_project) { Projects::ForkService.new(project, author_user).execute }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, target_project: project, author: author_user) }
+ let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") }
+
+ context 'as author' do
+ it 'shows direct edit link' do
+ login_as(author_user)
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
+ expect(page).to have_selector("[id=\"#{changelog_id}\"] a.js-edit-blob")
+ end
+ end
+
+ context 'as user who needs to fork' do
+ it 'shows fork/cancel confirmation' do
+ login_as(user)
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
+ find("[id=\"#{changelog_id}\"] .js-edit-blob").click
+
+ expect(page).to have_selector('.js-fork-suggestion-button', count: 1)
+ expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1)
+ end
+ end
+ end
end
diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb
index 55f3c1863ff..32a9082b9b9 100644
--- a/spec/features/merge_requests/filter_by_labels_spec.rb
+++ b/spec/features/merge_requests/filter_by_labels_spec.rb
@@ -3,7 +3,6 @@ require 'rails_helper'
feature 'Issue filtering by Labels', feature: true, js: true do
include FilteredSearchHelpers
include MergeRequestHelpers
- include WaitForAjax
let(:project) { create(:project, :public) }
let!(:user) { create(:user) }
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index 70e3997e716..2da60e9f4ad 100644
--- a/spec/features/merge_requests/filter_merge_requests_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -3,7 +3,6 @@ require 'rails_helper'
describe 'Filter merge requests', feature: true do
include FilteredSearchHelpers
include MergeRequestHelpers
- include WaitForAjax
let!(:project) { create(:project) }
let!(:group) { create(:group) }
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
index 84ad8765d8f..449a60c1d05 100644
--- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Mini Pipeline Graph', :js, :feature do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
index 9c4c0525267..99e283ac181 100644
--- a/spec/features/merge_requests/pipelines_spec.rb
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Pipelines for Merge Requests', feature: true, js: true do
- include WaitForAjax
-
given(:user) { create(:user) }
given(:merge_request) { create(:merge_request) }
given(:project) { merge_request.target_project }
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
index df5943f9136..275f81f50dc 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -3,7 +3,6 @@ require 'rails_helper'
feature 'Merge requests filter clear button', feature: true, js: true do
include FilteredSearchHelpers
include MergeRequestHelpers
- include WaitForAjax
include IssueHelpers
let!(:project) { create(:project, :public) }
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
index b56fdfe5611..9ecc998785b 100644
--- a/spec/features/merge_requests/update_merge_requests_spec.rb
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Multiple merge requests updating from merge_requests#index', feature: true do
- include WaitForAjax
-
let!(:user) { create(:user)}
let!(:project) { create(:project) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
diff --git a/spec/features/merge_requests/user_posts_notes.rb b/spec/features/merge_requests/user_posts_notes_spec.rb
index c7cc4d6bc72..c7cc4d6bc72 100644
--- a/spec/features/merge_requests/user_posts_notes.rb
+++ b/spec/features/merge_requests/user_posts_notes_spec.rb
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index a1f4eb2688b..1c0f21e5616 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
feature 'Merge Requests > User uses slash commands', feature: true, js: true do
include SlashCommandsHelpers
- include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 68a68f5d3f3..7a2da623c58 100644
--- a/spec/features/merge_requests/versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -107,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
@@ -129,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_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb
index 6676821b807..00d191ddf2c 100644
--- a/spec/features/merge_requests/widget_deployments_spec.rb
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Widget Deployments Header', feature: true, js: true do
- include WaitForAjax
-
describe 'when deployed to an environment' do
given(:user) { create(:user) }
given(:project) { merge_request.target_project }
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index a62c5435748..d918181a238 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
describe 'Merge request', :feature, :js do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -141,6 +139,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/milestone_spec.rb b/spec/features/milestone_spec.rb
index c3297de709a..c07de01c594 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Milestone', feature: true do
- include WaitForAjax
-
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb
index 2fa3e72ab08..50d7ca39045 100644
--- a/spec/features/milestones/milestones_spec.rb
+++ b/spec/features/milestones/milestones_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Milestone draggable', feature: true, js: true do
- include WaitForAjax
include DragTo
let(:milestone) { create(:milestone, project: project, title: 8.14) }
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index decad589c23..449ce80bc71 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'Member autocomplete', :js do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:note) { create(:note, noteable: noteable, project: noteable.project) }
@@ -36,6 +36,7 @@ feature 'Member autocomplete', :js do
end
context 'adding a new note on a Merge Request' do
+ let(:project) { create(:project, :public, :repository) }
let(:noteable) do
create(:merge_request, source_project: project,
target_project: project, author: author)
@@ -48,6 +49,7 @@ feature 'Member autocomplete', :js do
end
context 'adding a new note on a Commit' do
+ let(:project) { create(:project, :public, :repository) }
let(:noteable) { project.commit }
let(:note) { create(:note_on_commit, project: project, commit_id: project.commit.id) }
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 01cd268ffe8..8dba2ccbafa 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -1,22 +1,334 @@
require 'spec_helper'
-feature 'File blob', feature: true do
- include WaitForAjax
- include TreeHelper
+feature 'File blob', :js, feature: true do
+ let(:project) { create(:project, :public) }
- let(:project) { create(:project, :public, :test_repo) }
- let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') }
- let(:branch) { 'master' }
- let(:file_path) { project.repository.ls_files(project.repository.root_ref)[1] }
+ def visit_blob(path, fragment = nil)
+ visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment)
+ end
+
+ context 'Ruby file' do
+ before do
+ visit_blob('files/ruby/popen.rb')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows highlighted Ruby code
+ expect(page).to have_content("require 'fileutils'")
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+ end
+ end
+ end
+
+ context 'Markdown file' do
+ context 'visiting directly' do
+ before do
+ visit_blob('files/markdown/ruby-style-guide.md')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows rendered Markdown
+ expect(page).to have_link("PEP-8")
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # shows a disabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+
+ context 'switching to the rich viewer again' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+ end
+
+ context 'visiting with a line number anchor' do
+ before do
+ visit_blob('files/markdown/ruby-style-guide.md', 'L1')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # highlights the line in question
+ expect(page).to have_selector('#LC1.hll')
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+
+ context 'Markdown file (stored in LFS)' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add Markdown in LFS",
+ file_path: 'files/lfs/file.md',
+ file_content: project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data
+ ).execute
+ end
+
+ context 'when LFS is enabled on the project' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
+
+ visit_blob('files/lfs/file.md')
+
+ wait_for_ajax
+ end
+
+ it 'displays an error' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an error message
+ expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS. You can view the source or download it instead.')
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switcher .js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays an error' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows an error message
+ expect(page).to have_content('The source could not be displayed because it is stored in LFS. You can download it instead.')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ end
+ end
+ end
+ end
+
+ context 'when LFS is disabled on the project' do
+ before do
+ visit_blob('files/lfs/file.md')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows text
+ expect(page).to have_content('size 1575078')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+ end
+ end
+ end
+ end
+
+ context 'PDF file' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add PDF",
+ file_path: 'files/test.pdf',
+ file_content: project.repository.blob_at('add-pdf-file', 'files/pdf/test.pdf').data
+ ).execute
+
+ visit_blob('files/test.pdf')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows rendered PDF
+ expect(page).to have_selector('.js-pdf-viewer')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+ end
+
+ context 'ISO file (stored in LFS)' do
+ context 'when LFS is enabled on the project' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
+
+ visit_blob('files/lfs/lfs_object.iso')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows a download link
+ expect(page).to have_link('Download (1.5 MB)')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
+ end
+ end
+ end
- context 'anonymous' do
- context 'from blob file path' do
+ context 'when LFS is disabled on the project' do
before do
- visit namespace_project_blob_path(project.namespace, project, tree_join(branch, file_path))
+ visit_blob('files/lfs/lfs_object.iso')
+
+ wait_for_ajax
end
- it 'updates content' do
- expect(page).to have_link 'Edit'
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows text
+ expect(page).to have_content('size 1575078')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+ end
+ end
+ end
+ end
+
+ context 'ZIP file' do
+ before do
+ visit_blob('Gemfile.zip')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows a download link
+ expect(page).to have_link('Download (2.11 KB)')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
end
end
end
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index aab5a72678e..cc5b1a7e734 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
feature 'Editing file blob', feature: true, js: true do
- include WaitForAjax
include TreeHelper
let(:project) { create(:project, :public, :test_repo) }
diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb
index fa1a753afcb..d805450e095 100644
--- a/spec/features/projects/blobs/user_create_spec.rb
+++ b/spec/features/projects/blobs/user_create_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
feature 'New blob creation', feature: true, js: true do
- include WaitForAjax
include TargetBranchHelpers
given(:user) { create(:user) }
@@ -77,7 +76,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 +86,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/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 0b972d2a439..fa67d390c47 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-include WaitForAjax
describe 'Cherry-pick Commits' do
let(:group) { create(:group) }
@@ -75,8 +74,10 @@ describe 'Cherry-pick Commits' do
wait_for_ajax
- page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do
- click_link 'feature'
+ page.within('#modal-cherry-pick-commit .dropdown-menu') do
+ find('.dropdown-input input').set('feature')
+ wait_for_ajax
+ click_link "feature"
end
page.within('#modal-cherry-pick-commit') do
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 30a2b2bcf8c..98c0f2c63b0 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Mini Pipeline Graph in Commit View', :js, :feature do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
index 7c319af893b..a263781c43c 100644
--- a/spec/features/projects/edit_spec.rb
+++ b/spec/features/projects/edit_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Project edit', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index acc3efe04e6..1e12f8542e2 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -200,7 +200,7 @@ feature 'Environment', :feature do
end
scenario 'user deletes the branch with running environment' do
- visit namespace_project_branches_path(project.namespace, project)
+ visit namespace_project_branches_path(project.namespace, project, search: 'feature')
remove_branch_with_hooks(project, user, 'feature') do
page.within('.js-branch-feature') { find('a.btn-remove').click }
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 9079350186d..b080a8d500e 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -1,9 +1,6 @@
require 'spec_helper'
-include WaitForAjax
describe 'Edit Project Settings', feature: true do
- include WaitForAjax
-
let(:member) { create(:user) }
let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') }
let!(:issue) { create(:issue, project: project) }
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index d281043caa3..70e96efd557 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'user browses project', feature: true do
+feature 'user browses project', feature: true, js: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -13,7 +13,7 @@ feature 'user browses project', feature: true do
scenario "can see blame of '.gitignore'" do
click_link ".gitignore"
click_link 'Blame'
-
+
expect(page).to have_content "*.rb"
expect(page).to have_content "Dmitriy Zaporozhets"
expect(page).to have_content "Initial commit"
@@ -24,6 +24,7 @@ feature 'user browses project', feature: true do
click_link 'files'
click_link 'lfs'
click_link 'lfs_object.iso'
+ wait_for_ajax
expect(page).not_to have_content 'Download (1.5 MB)'
expect(page).to have_content 'version https://git-lfs.github.com/spec/v1'
diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb
index 5d7bd3dc4ce..69744ac3948 100644
--- a/spec/features/projects/files/creating_a_file_spec.rb
+++ b/spec/features/projects/files/creating_a_file_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'User wants to create a file', feature: true do
- include WaitForAjax
-
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -29,16 +27,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/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index 32f33a3ca97..548131c7cd4 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -1,13 +1,14 @@
require 'spec_helper'
+require 'fileutils'
feature 'User wants to add a Dockerfile file', feature: true do
- include WaitForAjax
-
before do
user = create(:user)
project = create(:project)
project.team << [user, :master]
+
login_as user
+
visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'Dockerfile')
end
@@ -17,11 +18,14 @@ feature 'User wants to add a Dockerfile file', feature: true do
scenario 'user can pick a Dockerfile file from the dropdown', js: true do
find('.js-dockerfile-selector').click
+
wait_for_ajax
+
within '.dockerfile-selector' do
find('.dropdown-input-field').set('HTTPd')
find('.dropdown-content li', text: 'HTTPd').click
end
+
wait_for_ajax
expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd')
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
index 3e544316f28..7a3afafec29 100644
--- a/spec/features/projects/files/editing_a_file_spec.rb
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -1,14 +1,12 @@
require 'spec_helper'
feature 'User wants to edit a file', feature: true do
- include WaitForAjax
-
let(:project) { create(:project) }
let(:user) { create(:user) }
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/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
index 10b91d8990b..5c8105de4cb 100644
--- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
+++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'User views files page', feature: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:forked_project_with_submodules) }
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
index 582349d8d5b..e7a6749d8ac 100644
--- a/spec/features/projects/files/find_file_keyboard_spec.rb
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Find file keyboard shortcuts', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/projects/files/find_files_spec.rb b/spec/features/projects/files/find_files_spec.rb
new file mode 100644
index 00000000000..716b7591b95
--- /dev/null
+++ b/spec/features/projects/files/find_files_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+feature 'Find files button in the tree header', feature: true do
+ given(:user) { create(:user) }
+ given(:project) { create(:project) }
+
+ background do
+ login_as(user)
+ project.team << [user, :developer]
+ end
+
+ scenario 'project main screen' do
+ visit namespace_project_path(
+ project.namespace,
+ project
+ )
+
+ expect(page).to have_selector('.tree-controls .shortcuts-find-file')
+ end
+
+ scenario 'project tree screen' do
+ visit namespace_project_tree_path(
+ project.namespace,
+ project,
+ project.default_branch
+ )
+
+ expect(page).to have_selector('.tree-controls .shortcuts-find-file')
+ end
+end
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index 9ebef505b92..e59428f8b24 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'User wants to add a .gitignore file', feature: true do
- include WaitForAjax
-
before do
user = create(:user)
project = create(:project)
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
index fca40f68b01..85b66b93fba 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'User wants to add a .gitlab-ci.yml file', feature: true do
- include WaitForAjax
-
before do
user = create(:user)
project = create(:project)
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 8ff0f5898ec..249830921ac 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'project owner creates a license file', feature: true, js: true do
- include WaitForAjax
-
let(:project_master) { create(:user) }
let(:project) { create(:project) }
background do
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 1a1910455a1..70a41886985 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'project owner sees a link to create a license file in empty project', feature: true, js: true do
- include WaitForAjax
-
let(:project_master) { create(:user) }
let(:project) { create(:empty_project) }
background do
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
index c51851d3f94..cd3af0b7d29 100644
--- a/spec/features/projects/files/undo_template_spec.rb
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -1,26 +1,25 @@
require 'spec_helper'
-include WaitForAjax
-feature 'Template Undo Button', js: true do
+feature 'Template Undo Button', js: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.team << [user, :master]
- login_as user
+ login_as user
end
-
- context 'editing a matching file and applying a template' do
+
+ context 'editing a matching file and applying a template' do
before do
- visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, "LICENSE"))
+ visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, "LICENSE"))
select_file_template('.js-license-selector', 'Apache License 2.0')
end
-
+
scenario 'reverts template application' do
try_template_undo('http://www.apache.org/licenses/', 'Apply a license template')
end
end
-
+
context 'creating a non-matching file' do
before do
visit namespace_project_new_blob_path(project.namespace, project, 'master')
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 62d0aedda48..d28a853bbc2 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'issuable templates', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
@@ -163,12 +161,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/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 1e900d7e660..836f81fb16d 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
feature 'Prioritize labels', feature: true do
- include WaitForAjax
include DragTo
let(:user) { create(:user) }
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
index cffb935ad5a..ab2b089db2e 100644
--- a/spec/features/projects/members/group_links_spec.rb
+++ b/spec/features/projects/members/group_links_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Projects > Members > Anonymous user sees members', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:empty_project, :public) }
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/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index c3f45be6e4b..19d14ad9af4 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do
- include WaitForAjax
include Select2Helper
include ActiveSupport::Testing::TimeHelpers
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index de25d45f447..1bf8f710b9f 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -31,6 +31,17 @@ feature 'Projects > Members > User requests access', feature: true do
expect(page).not_to have_content 'Leave Project'
end
+ context 'code access is restricted' do
+ scenario 'user can request access' do
+ project.project_feature.update!(repository_access_level: ProjectFeature::PRIVATE,
+ builds_access_level: ProjectFeature::PRIVATE,
+ merge_requests_access_level: ProjectFeature::PRIVATE)
+ visit namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content 'Request Access'
+ end
+ end
+
scenario 'user is not listed in the project members page' do
click_link 'Request Access'
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
index 05f3162f13c..1370ab1c521 100644
--- a/spec/features/projects/merge_request_button_spec.rb
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -85,8 +85,8 @@ feature 'Merge Request button', feature: true do
context 'on branches page' do
it_behaves_like 'Merge request button only shown when allowed' do
let(:label) { 'Merge request' }
- let(:url) { namespace_project_branches_path(project.namespace, project) }
- let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project) }
+ let(:url) { namespace_project_branches_path(project.namespace, project, search: 'feature') }
+ let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project, search: 'feature') }
end
end
diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb
index dab78fd3571..b4fc0edbde8 100644
--- a/spec/features/projects/milestones/milestone_spec.rb
+++ b/spec/features/projects/milestones/milestone_spec.rb
@@ -63,4 +63,27 @@ feature 'Project milestone', :feature do
expect(page).not_to have_content('Assign some issues to this milestone.')
end
end
+
+ context 'when project has an issue' do
+ before do
+ create(:issue, project: project, milestone: milestone)
+
+ visit namespace_project_milestone_path(project.namespace, project, milestone)
+ end
+
+ describe 'the collapsed sidebar' do
+ before do
+ find('.milestone-sidebar .gutter-toggle').click
+ end
+
+ it 'shows the total MR and issue counts' do
+ find('.milestone-sidebar .block', match: :first)
+
+ aggregate_failures 'MR and issue blocks' do
+ expect(find('.milestone-sidebar .block.issues')).to have_content 1
+ expect(find('.milestone-sidebar .block.merge-requests')).to have_content 0
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index 3b8f0b2d3f8..881ad7910dd 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
feature 'Ref switcher', feature: true, js: true do
- include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb
new file mode 100644
index 00000000000..7909234556e
--- /dev/null
+++ b/spec/features/projects/settings/integration_settings_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+feature 'Integration settings', feature: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+ let(:integrations_path) { namespace_project_settings_integrations_path(project.namespace, project) }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ context 'for developer' do
+ given(:role) { :developer }
+
+ scenario 'to be disallowed to view' do
+ visit integrations_path
+
+ expect(page.status_code).to eq(404)
+ end
+ end
+
+ context 'for master' do
+ given(:role) { :master }
+
+ context 'Webhooks' do
+ let(:hook) { create(:project_hook, :all_events_enabled, enable_ssl_verification: true, project: project) }
+ let(:url) { generate(:url) }
+
+ scenario 'show list of webhooks' do
+ hook
+
+ visit integrations_path
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_content(hook.url)
+ expect(page).to have_content('SSL Verification: enabled')
+ expect(page).to have_content('Push Events')
+ expect(page).to have_content('Tag Push Events')
+ expect(page).to have_content('Issues Events')
+ expect(page).to have_content('Confidential Issues Events')
+ expect(page).to have_content('Note Events')
+ expect(page).to have_content('Merge Requests Events')
+ expect(page).to have_content('Pipeline Events')
+ expect(page).to have_content('Wiki Page Events')
+ end
+
+ scenario 'create webhook' do
+ visit integrations_path
+
+ fill_in 'hook_url', with: url
+ check 'Tag push events'
+ check 'Enable SSL verification'
+
+ click_button 'Add webhook'
+
+ expect(page).to have_content(url)
+ expect(page).to have_content('SSL Verification: enabled')
+ expect(page).to have_content('Push Events')
+ expect(page).to have_content('Tag Push Events')
+ end
+
+ scenario 'edit existing webhook' do
+ hook
+ visit integrations_path
+
+ click_link 'Edit'
+ fill_in 'hook_url', with: url
+ check 'Enable SSL verification'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'SSL Verification: enabled'
+ expect(page).to have_content(url)
+ end
+
+ scenario 'test existing webhook' do
+ WebMock.stub_request(:post, hook.url)
+ visit integrations_path
+
+ click_link 'Test'
+
+ expect(current_path).to eq(integrations_path)
+ end
+
+ scenario 'remove existing webhook' do
+ hook
+ visit integrations_path
+
+ expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1)
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb
new file mode 100644
index 00000000000..7eb1210e307
--- /dev/null
+++ b/spec/features/projects/snippets/show_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+feature 'Project snippet', :js, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:snippet) { create(:project_snippet, project: project, file_name: file_name, content: content) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'Ruby file' do
+ let(:file_name) { 'popen.rb' }
+ let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
+
+ before do
+ visit namespace_project_snippet_path(project.namespace, project, snippet)
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows highlighted Ruby code
+ expect(page).to have_content("require 'fileutils'")
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+
+ context 'Markdown file' do
+ let(:file_name) { 'ruby-style-guide.md' }
+ let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data }
+
+ context 'visiting directly' do
+ before do
+ visit namespace_project_snippet_path(project.namespace, project, snippet)
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows rendered Markdown
+ expect(page).to have_link("PEP-8")
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # shows a disabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+
+ context 'switching to the rich viewer again' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+ end
+
+ context 'visiting with a line number anchor' do
+ before do
+ visit namespace_project_snippet_path(project.namespace, project, snippet, anchor: 'L1')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # highlights the line in question
+ expect(page).to have_selector('#LC1.hll')
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/user_create_dir_spec.rb b/spec/features/projects/user_create_dir_spec.rb
index 2065abfb248..5dfdc465d7d 100644
--- a/spec/features/projects/user_create_dir_spec.rb
+++ b/spec/features/projects/user_create_dir_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
feature 'New directory creation', feature: true, js: true do
- include WaitForAjax
include TargetBranchHelpers
given(:user) { create(:user) }
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index ce5c5f21167..b7a41ca54e6 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'View on environment', js: true do
- include WaitForAjax
-
let(:branch_name) { 'feature' }
let(:file_path) { 'files/ruby/feature.rb' }
let(:project) { create(:project, :repository) }
@@ -25,7 +23,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 +34,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/features/projects_spec.rb b/spec/features/projects_spec.rb
index ba56030e28d..060e19596ae 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
feature 'Project', feature: true do
describe 'description' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:path) { namespace_project_path(project.namespace, project) }
before do
@@ -36,7 +36,7 @@ feature 'Project', feature: true do
describe 'remove forked relationship', js: true do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
before do
login_with user
@@ -57,7 +57,7 @@ feature 'Project', feature: true do
describe 'removal', js: true do
let(:user) { create(:user, username: 'test', name: 'test') }
- let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
+ let(:project) { create(:empty_project, namespace: user.namespace, name: 'project1') }
before do
login_with(user)
@@ -75,10 +75,8 @@ feature 'Project', feature: true do
end
describe 'project title' do
- include WaitForAjax
-
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
before do
login_with(user)
@@ -94,8 +92,8 @@ feature 'Project', feature: true do
describe 'project title' do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
- let(:project2) { create(:project, namespace: user.namespace, path: 'test') }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
+ let(:project2) { create(:empty_project, namespace: user.namespace, path: 'test') }
let(:issue) { create(:issue, project: project) }
context 'on issues page', js: true do
diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb
index eb3cea775da..d30e7947106 100644
--- a/spec/features/protected_branches/access_control_ce_spec.rb
+++ b/spec/features/protected_branches/access_control_ce_spec.rb
@@ -9,7 +9,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
allowed_to_push_button = find(".js-allowed-to-push")
unless allowed_to_push_button.text == access_type_name
- allowed_to_push_button.click
+ allowed_to_push_button.trigger('click')
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 1a3f7b970f6..fc9b293c393 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -2,15 +2,13 @@ require 'spec_helper'
Dir["./spec/features/protected_branches/*.rb"].sort.each { |f| require f }
feature 'Projected Branches', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user, :admin) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before { login_as(user) }
def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").click
+ find(".js-protected-branch-select").trigger('click')
find(".dropdown-input-field").set(branch_name)
click_on("Create wildcard #{branch_name}")
end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index 09e8c850de3..e3aa87ded28 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -2,10 +2,8 @@ require 'spec_helper'
Dir["./spec/features/protected_tags/*.rb"].sort.each { |f| require f }
feature 'Projected Tags', feature: true, js: true do
- include WaitForAjax
-
let(:user) { create(:user, :admin) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before { login_as(user) }
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index e8ad28a00f0..da6388dcdf2 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -2,10 +2,9 @@ require 'spec_helper'
describe "Search", feature: true do
include FilteredSearchHelpers
- include WaitForAjax
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
let!(:issue) { create(:issue, project: project, assignee: user) }
let!(:issue2) { create(:issue, project: project, author: user) }
@@ -62,6 +61,7 @@ describe "Search", feature: true do
context 'search for comments' do
context 'when comment belongs to a invalid commit' do
+ let(:project) { create(:project, :repository) }
let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') }
before { note.update_attributes(commit_id: 12345678) }
@@ -103,6 +103,7 @@ describe "Search", feature: true do
end
it 'finds a commit' do
+ project = create(:project, :repository) { |p| p.add_reporter(user) }
visit namespace_project_path(project.namespace, project)
page.within '.search' do
@@ -116,6 +117,7 @@ describe "Search", feature: true do
end
it 'finds a code' do
+ project = create(:project, :repository) { |p| p.add_reporter(user) }
visit namespace_project_path(project.namespace, project)
page.within '.search' do
@@ -222,6 +224,8 @@ describe "Search", feature: true do
end
describe 'search for commits' do
+ let(:project) { create(:project, :repository) }
+
before do
visit search_path(project_id: project.id)
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 6ecdc8cbb71..a1a36931824 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -399,6 +399,44 @@ describe "Internal Project Access", feature: true do
end
end
+ describe 'GET /:project_path/builds/:id/trace' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ subject { trace_namespace_project_build_path(project.namespace, project, build.id) }
+
+ context 'when allowed for public and internal' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
+ context 'when disallowed for public and internal' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+ end
+
describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index a8fc0624588..5d58494a22a 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -388,6 +388,38 @@ describe "Private Project Access", feature: true do
end
end
+ describe 'GET /:project_path/builds/:id/trace' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ subject { trace_namespace_project_build_path(project.namespace, project, build.id) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+
+ context 'when public builds is enabled' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ end
+
+ context 'when public builds is disabled' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ end
+ end
+
describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index c4d2f50ca14..5df5b710dc4 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -219,6 +219,44 @@ describe "Public Project Access", feature: true do
end
end
+ describe 'GET /:project_path/builds/:id/trace' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ subject { trace_namespace_project_build_path(project.namespace, project, build.id) }
+
+ context 'when allowed for public' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
+ end
+
+ context 'when disallowed for public' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+ end
+
describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) }
diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb
index 5470276bf06..9409c323288 100644
--- a/spec/features/snippets/create_snippet_spec.rb
+++ b/spec/features/snippets/create_snippet_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Create Snippet', feature: true do
+feature 'Create Snippet', :js, feature: true do
before do
login_as :user
visit new_snippet_path
@@ -9,10 +9,11 @@ feature 'Create Snippet', feature: true do
scenario 'Authenticated user creates a snippet' do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
page.within('.file-editor') do
- find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!'
+ find('.ace_editor').native.send_keys 'Hello World!'
end
click_button 'Create snippet'
+ wait_for_ajax
expect(page).to have_content('My Snippet Title')
expect(page).to have_content('Hello World!')
@@ -22,10 +23,11 @@ feature 'Create Snippet', feature: true do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
page.within('.file-editor') do
find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
- find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!'
+ find('.ace_editor').native.send_keys 'Hello World!'
end
click_button 'Create snippet'
+ wait_for_ajax
expect(page).to have_content('My Snippet Title')
expect(page).to have_content('snippet+file+name')
diff --git a/spec/features/snippets/public_snippets_spec.rb b/spec/features/snippets/public_snippets_spec.rb
index 34300ccb940..2df483818c3 100644
--- a/spec/features/snippets/public_snippets_spec.rb
+++ b/spec/features/snippets/public_snippets_spec.rb
@@ -1,10 +1,11 @@
require 'rails_helper'
-feature 'Public Snippets', feature: true do
+feature 'Public Snippets', :js, feature: true do
scenario 'Unauthenticated user should see public snippets' do
public_snippet = create(:personal_snippet, :public)
visit snippet_path(public_snippet)
+ wait_for_ajax
expect(page).to have_content(public_snippet.content)
end
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
new file mode 100644
index 00000000000..cebcba6a230
--- /dev/null
+++ b/spec/features/snippets/show_spec.rb
@@ -0,0 +1,126 @@
+require 'spec_helper'
+
+feature 'Snippet', :js, feature: true do
+ let(:project) { create(:project, :repository) }
+ let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) }
+
+ context 'Ruby file' do
+ let(:file_name) { 'popen.rb' }
+ let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
+
+ before do
+ visit snippet_path(snippet)
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows highlighted Ruby code
+ expect(page).to have_content("require 'fileutils'")
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+
+ context 'Markdown file' do
+ let(:file_name) { 'ruby-style-guide.md' }
+ let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data }
+
+ context 'visiting directly' do
+ before do
+ visit snippet_path(snippet)
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows rendered Markdown
+ expect(page).to have_link("PEP-8")
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # shows a disabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+
+ context 'switching to the rich viewer again' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the rich viewer' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+ end
+
+ context 'visiting with a line number anchor' do
+ before do
+ visit snippet_path(snippet, anchor: 'L1')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob using the simple viewer' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # highlights the line in question
+ expect(page).to have_selector('#LC1.hll')
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index a5d14aa19f1..c33692fc4a9 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Task Lists', feature: true do
include Warden::Test::Helpers
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -240,6 +240,7 @@ feature 'Task Lists', feature: true do
end
describe 'multiple tasks' do
+ let(:project) { create(:project, :repository) }
let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
it 'renders for description' do
diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb
index e8f06916d53..f32e70c2c3f 100644
--- a/spec/features/todos/todos_filtering_spec.rb
+++ b/spec/features/todos/todos_filtering_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'Dashboard > User filters todos', feature: true, js: true do
- include WaitForAjax
-
let(:user_1) { create(:user, username: 'user_1', name: 'user_1') }
let(:user_2) { create(:user, username: 'user_2', name: 'user_2') }
@@ -47,8 +45,8 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
wait_for_ajax
- expect(find('.todos-list')).to have_content user_1.name
- expect(find('.todos-list')).not_to have_content user_2.name
+ expect(find('.todos-list')).to have_content 'merge request'
+ expect(find('.todos-list')).not_to have_content 'issue'
end
it "shows only authors of existing todos" do
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index c270511c903..be5b3af417f 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'Dashboard Todos', feature: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
@@ -101,6 +99,83 @@ describe 'Dashboard Todos', feature: true do
end
end
+ context 'User created todos for themself' do
+ before do
+ login_as(user)
+ end
+
+ context 'issue assigned todo' do
+ before do
+ create(:todo, :assigned, user: user, project: project, target: issue, author: user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows issue assigned to yourself message' do
+ page.within('.js-todos-all') do
+ expect(page).to have_content("You assigned issue #{issue.to_reference(full: true)} to yourself")
+ end
+ end
+ end
+
+ context 'marked todo' do
+ before do
+ create(:todo, :marked, user: user, project: project, target: issue, author: user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows you added a todo message' do
+ page.within('.js-todos-all') do
+ expect(page).to have_content("You added a todo for issue #{issue.to_reference(full: true)}")
+ expect(page).not_to have_content('to yourself')
+ end
+ end
+ end
+
+ context 'mentioned todo' do
+ before do
+ create(:todo, :mentioned, user: user, project: project, target: issue, author: user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows you mentioned yourself message' do
+ page.within('.js-todos-all') do
+ expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference(full: true)}")
+ expect(page).not_to have_content('to yourself')
+ end
+ end
+ end
+
+ context 'directly_addressed todo' do
+ before do
+ create(:todo, :directly_addressed, user: user, project: project, target: issue, author: user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows you directly addressed yourself message' do
+ page.within('.js-todos-all') do
+ expect(page).to have_content("You directly addressed yourself on issue #{issue.to_reference(full: true)}")
+ expect(page).not_to have_content('to yourself')
+ end
+ end
+ end
+
+ context 'approval todo' do
+ let(:merge_request) { create(:merge_request) }
+
+ before do
+ create(:todo, :approval_required, user: user, project: project, target: merge_request, author: user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows you set yourself as an approver message' do
+ page.within('.js-todos-all') do
+ expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference(full: true)}")
+ expect(page).not_to have_content('to yourself')
+ end
+ end
+ end
+ end
+
context 'User has done todos', js: true do
before do
create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author)
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index c877cfdd978..544d2dcb87f 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
- include WaitForAjax
-
before { allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) }
def manage_two_factor_authentication
diff --git a/spec/features/users/projects_spec.rb b/spec/features/users/projects_spec.rb
index 1d75fe434b0..373b64808f8 100644
--- a/spec/features/users/projects_spec.rb
+++ b/spec/features/users/projects_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'Projects tab on a user profile', :feature, :js do
- include WaitForAjax
-
let(:user) { create(:user) }
let!(:project) { create(:empty_project, namespace: user.namespace) }
let!(:project2) { create(:empty_project, namespace: user.namespace) }
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index ce7e809ec76..1546a06b80c 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'Snippets tab on a user profile', feature: true, js: true do
- include WaitForAjax
-
context 'when the user has snippets' do
let(:user) { create(:user) }
let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 2de0fbe7ab2..c43feadc808 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -68,7 +68,6 @@ feature 'Users', feature: true, js: true do
end
feature 'username validation' do
- include WaitForAjax
let(:loading_icon) { '.fa.fa-spinner' }
let(:username_input) { 'new_user_username' }
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index a362d6fd3b6..b83a230c1f8 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe 'Project variables', js: true do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
before do
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 231fd85c464..a1ae1d746af 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -1,24 +1,24 @@
require 'spec_helper'
describe IssuesFinder do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:project1) { create(:empty_project) }
- let(:project2) { create(:empty_project) }
- let(:milestone) { create(:milestone, project: project1) }
- let(:label) { create(:label, project: project2) }
- let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
- let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
- let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2, title: 'tanuki', description: 'tanuki') }
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+ set(:project1) { create(:empty_project) }
+ set(:project2) { create(:empty_project) }
+ set(:milestone) { create(:milestone, project: project1) }
+ set(:label) { create(:label, project: project2) }
+ set(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
+ set(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
+ set(:issue3) { create(:issue, author: user2, assignee: user2, project: project2, title: 'tanuki', description: 'tanuki') }
describe '#execute' do
- let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') }
- let!(:label_link) { create(:label_link, label: label, target: issue2) }
+ set(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') }
+ set(:label_link) { create(:label_link, label: label, target: issue2) }
let(:search_user) { user }
let(:params) { {} }
let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
- before do
+ before(:context) do
project1.team << [user, :master]
project2.team << [user, :developer]
project2.team << [user2, :developer]
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/public.json b/spec/fixtures/api/schemas/public_api/v4/user/public.json
index 5587cfec61a..faa126b65f2 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/public.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/public.json
@@ -9,7 +9,6 @@
"avatar_url",
"web_url",
"created_at",
- "is_admin",
"bio",
"location",
"skype",
@@ -43,7 +42,6 @@
"avatar_url": { "type": "string" },
"web_url": { "type": "string" },
"created_at": { "type": "date" },
- "is_admin": { "type": "boolean" },
"bio": { "type": ["string", "null"] },
"location": { "type": ["string", "null"] },
"skype": { "type": "string" },
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 0cdbc32431d..51a3e91d201 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -116,7 +116,7 @@ Linking to a file relative to this project's repository should work.
Because life would be :zzz: without Emoji, right? :rocket:
-Get ready for the Emoji :bomb:: :+1::-1::ok_hand::wave::v::raised_hand::muscle:
+Get ready for the Emoji :bomb: : :+1: :-1: :ok_hand: :wave: :v: :raised_hand: :muscle:
### TableOfContentsFilter
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 5c07ea8a872..01bdf01ad22 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -239,33 +239,6 @@ describe ApplicationHelper do
end
end
- describe 'render_markup' do
- let(:content) { 'Noël' }
- let(:user) { create(:user) }
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
- it 'preserves encoding' do
- expect(content.encoding.name).to eq('UTF-8')
- expect(helper.render_markup('foo.rst', content).encoding.name).to eq('UTF-8')
- end
-
- it "delegates to #markdown when file name corresponds to Markdown" do
- expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
- expect(helper).to receive(:markdown).and_return('NOEL')
-
- expect(helper.render_markup('foo.md', content)).to eq('NOEL')
- end
-
- it "delegates to #asciidoc when file name corresponds to AsciiDoc" do
- expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
- expect(helper).to receive(:asciidoc).and_return('NOEL')
-
- expect(helper.render_markup('foo.adoc', content)).to eq('NOEL')
- end
- end
-
describe '#active_when' do
it { expect(helper.active_when(true)).to eq('active') }
it { expect(helper.active_when(false)).to eq(nil) }
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 508aeb7cf67..075f1887d91 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -56,15 +56,14 @@ describe BlobHelper do
end
end
- describe "#sanitize_svg" do
+ describe "#sanitize_svg_data" do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
let(:data) { open(input_svg_path).read }
let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
let(:expected) { open(expected_svg_path).read }
it 'retains essential elements' do
- blob = OpenStruct.new(data: data)
- expect(sanitize_svg(blob).data).to eq(expected)
+ expect(sanitize_svg_data(data)).to eq(expected)
end
end
@@ -105,4 +104,120 @@ describe BlobHelper do
expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10')
end
end
+
+ context 'viewer related' do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ let(:viewer_class) do
+ Class.new(BlobViewer::Base) do
+ self.max_size = 1.megabyte
+ self.absolute_max_size = 5.megabytes
+ self.type = :rich
+ self.client_side = false
+ end
+ end
+
+ let(:viewer) { viewer_class.new(blob) }
+ let(:blob) { fake_blob }
+
+ describe '#blob_render_error_reason' do
+ context 'for error :too_large' do
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'returns an error message' do
+ expect(helper.blob_render_error_reason(viewer)).to eq('it is larger than 5 MB')
+ end
+ end
+
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(size: 2.megabytes) }
+
+ it 'returns an error message' do
+ expect(helper.blob_render_error_reason(viewer)).to eq('it is larger than 1 MB')
+ end
+ end
+ end
+
+ context 'for error :server_side_but_stored_in_lfs' do
+ let(:blob) { fake_blob(lfs: true) }
+
+ it 'returns an error message' do
+ expect(helper.blob_render_error_reason(viewer)).to eq('it is stored in LFS')
+ end
+ end
+ end
+
+ describe '#blob_render_error_options' do
+ before do
+ assign(:project, project)
+ assign(:blob, blob)
+ assign(:id, File.join('master', blob.path))
+
+ controller.params[:controller] = 'projects/blob'
+ controller.params[:action] = 'show'
+ controller.params[:namespace_id] = project.namespace.to_param
+ controller.params[:project_id] = project.to_param
+ controller.params[:id] = File.join('master', blob.path)
+ end
+
+ context 'for error :too_large' do
+ context 'when the max size can be overridden' do
+ let(:blob) { fake_blob(size: 2.megabytes) }
+
+ it 'includes a "load it anyway" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/load it anyway/)
+ end
+ end
+
+ context 'when the max size cannot be overridden' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'does not include a "load it anyway" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/load it anyway/)
+ end
+ end
+ end
+
+ context 'when the viewer is rich' do
+ context 'the blob is rendered as text' do
+ let(:blob) { fake_blob(path: 'file.md', lfs: true) }
+
+ it 'includes a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/view the source/)
+ end
+ end
+
+ context 'the blob is not rendered as text' do
+ let(:blob) { fake_blob(path: 'file.pdf', binary: true, lfs: true) }
+
+ it 'does not include a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
+ end
+ end
+ end
+
+ context 'when the viewer is not rich' do
+ before do
+ viewer_class.type = :simple
+ end
+
+ let(:blob) { fake_blob(path: 'file.md', lfs: true) }
+
+ it 'does not include a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
+ end
+ end
+
+ it 'includes a "download it" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/download it/)
+ end
+ end
+ end
end
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index c795fe5a2a3..e6bb953e9d8 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -6,23 +6,53 @@ describe CiStatusHelper do
let(:success_commit) { double("Ci::Pipeline", status: 'success') }
let(:failed_commit) { double("Ci::Pipeline", status: 'failed') }
- describe 'ci_icon_for_status' do
+ describe '#ci_icon_for_status' do
it 'renders to correct svg on success' do
- expect(helper).to receive(:render).with('shared/icons/icon_status_success.svg', anything)
+ expect(helper).to receive(:render)
+ .with('shared/icons/icon_status_success.svg', anything)
+
helper.ci_icon_for_status(success_commit.status)
end
+
it 'renders the correct svg on failure' do
- expect(helper).to receive(:render).with('shared/icons/icon_status_failed.svg', anything)
+ expect(helper).to receive(:render)
+ .with('shared/icons/icon_status_failed.svg', anything)
+
helper.ci_icon_for_status(failed_commit.status)
end
end
+ describe '#ci_text_for_status' do
+ context 'when status is manual' do
+ it 'changes the status to blocked' do
+ expect(helper.ci_text_for_status('manual'))
+ .to eq 'blocked'
+ end
+ end
+
+ context 'when status is success' do
+ it 'changes the status to passed' do
+ expect(helper.ci_text_for_status('success'))
+ .to eq 'passed'
+ end
+ end
+
+ context 'when status is something else' do
+ it 'returns status unchanged' do
+ expect(helper.ci_text_for_status('some-status'))
+ .to eq 'some-status'
+ end
+ end
+ end
+
describe "#pipeline_status_cache_key" do
it "builds a cache key for pipeline status" do
pipeline_status = Gitlab::Cache::Ci::ProjectPipelineStatus.new(
build(:project),
- sha: "123abc",
- status: "success"
+ pipeline_info: {
+ sha: "123abc",
+ status: "success"
+ }
)
expect(helper.pipeline_status_cache_key(pipeline_status)).to eq("pipeline-status/123abc-success")
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index a7c3c281083..c3bd0cb3542 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -56,7 +56,7 @@ describe EventsHelper do
it 'preserves code color scheme' do
input = "```ruby\ndef test\n 'hello world'\nend\n```"
- expected = '<pre class="code highlight js-syntax-highlight ruby">' \
+ expected = "\n<pre class=\"code highlight js-syntax-highlight ruby\">" \
"<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
"</code></pre>"
expect(helper.event_note(input)).to eq(expected)
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index c052981fe73..91c8faea7fd 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -1,6 +1,21 @@
require 'spec_helper'
describe IconsHelper do
+ describe 'icon' do
+ it 'returns aria-hidden by default' do
+ star = icon('star')
+
+ expect(star['aria-hidden']).to eq 'aria-hidden'
+ end
+
+ it 'does not return aria-hidden if aria-label is set' do
+ up = icon('up', 'aria-label' => 'up')
+
+ expect(up['aria-hidden']).to be_nil
+ expect(up['aria-label']).to eq 'aria-label'
+ end
+ end
+
describe 'file_type_icon_class' do
it 'returns folder class' do
expect(file_type_icon_class('folder', 0, 'folder_name')).to eq 'folder'
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 6cf3f86680a..c10f4b09b5b 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe GitlabMarkdownHelper do
- include ApplicationHelper
-
+describe MarkupHelper do
let!(:project) { create(:project, :repository) }
let(:user) { create(:user, username: 'gfm') }
@@ -111,9 +109,9 @@ describe GitlabMarkdownHelper do
end
it 'replaces commit message with emoji to link' do
- actual = link_to_gfm(':book:Book', '/foo')
+ actual = link_to_gfm(':book: Book', '/foo')
expect(actual).
- to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo">Book</a>'
+ to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>'
end
end
@@ -128,7 +126,7 @@ describe GitlabMarkdownHelper do
it "uses Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown)
- expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki, page_slug: "nested/page")
+ expect(helper).to receive(:markdown_unsafe).with('wiki content', pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page")
helper.render_wiki_content(@wiki)
end
@@ -136,7 +134,7 @@ describe GitlabMarkdownHelper do
it "uses Asciidoctor for asciidoc files" do
allow(@wiki).to receive(:format).and_return(:asciidoc)
- expect(helper).to receive(:asciidoc).with('wiki content')
+ expect(helper).to receive(:asciidoc_unsafe).with('wiki content')
helper.render_wiki_content(@wiki)
end
@@ -151,6 +149,29 @@ describe GitlabMarkdownHelper do
end
end
+ describe 'markup' do
+ let(:content) { 'Noël' }
+
+ it 'preserves encoding' do
+ expect(content.encoding.name).to eq('UTF-8')
+ expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8')
+ end
+
+ it "delegates to #markdown_unsafe when file name corresponds to Markdown" do
+ expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
+ expect(helper).to receive(:markdown_unsafe).and_return('NOEL')
+
+ expect(helper.markup('foo.md', content)).to eq('NOEL')
+ end
+
+ it "delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc" do
+ expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
+ expect(helper).to receive(:asciidoc_unsafe).and_return('NOEL')
+
+ expect(helper.markup('foo.adoc', content)).to eq('NOEL')
+ end
+ end
+
describe '#first_line_in_markdown' do
it 'truncates Markdown properly' do
text = "@#{user.username}, can you look at this?\nHello world\n"
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 25f23826648..10681af5f7e 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -22,24 +22,51 @@ describe MergeRequestsHelper do
end
describe '#issues_sentence' do
+ let(:project) { create :project }
+
subject { issues_sentence(issues) }
let(:issues) do
- [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)]
+ [build(:issue, iid: 2, project: project),
+ build(:issue, iid: 3, project: project),
+ build(:issue, iid: 1, project: project)]
end
- it { is_expected.to eq('#1, #2, and #3') }
+ it do
+ @project = project
+
+ is_expected.to eq('#1, #2, and #3')
+ end
context 'for JIRA issues' do
let(:project) { create(:empty_project) }
let(:issues) do
[
- ExternalIssue.new('JIRA-123', project),
ExternalIssue.new('JIRA-456', project),
- ExternalIssue.new('FOOBAR-7890', project)
+ ExternalIssue.new('FOOBAR-7890', project),
+ ExternalIssue.new('JIRA-123', project)
]
end
- it { is_expected.to eq('FOOBAR-7890, JIRA-123, and JIRA-456') }
+ it do
+ @project = project
+ is_expected.to eq('FOOBAR-7890, JIRA-123, and JIRA-456')
+ end
+ end
+
+ context 'for issues from multiple namespaces' do
+ let(:project) { create(:project) }
+ let(:other_project) { create(:project) }
+ let(:issues) do
+ [build(:issue, iid: 2, project: project),
+ build(:issue, iid: 3, project: other_project),
+ build(:issue, iid: 1, project: project)]
+ end
+
+ it do
+ @project = project
+
+ is_expected.to eq("#1, #2, and #{other_project.namespace.path}/#{other_project.path}#3")
+ end
end
end
@@ -122,6 +149,50 @@ describe MergeRequestsHelper do
end
end
+ describe '#target_projects' do
+ let(:project) { create(:empty_project) }
+ let(:fork_project) { create(:empty_project, forked_from_project: project) }
+
+ context 'when target project has enabled merge requests' do
+ it 'returns the forked_from project' do
+ expect(target_projects(fork_project)).to contain_exactly(project, fork_project)
+ end
+ end
+
+ context 'when target project has disabled merge requests' do
+ it 'returns the forked project' do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ expect(target_projects(fork_project)).to contain_exactly(fork_project)
+ end
+ end
+ end
+
+ describe '#new_mr_path_from_push_event' do
+ subject(:url_params) { URI.decode_www_form(new_mr_path_from_push_event(event)).to_h }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, creator: user) }
+ let(:fork_project) { create(:project, forked_from_project: project, creator: user) }
+ let(:event) do
+ push_data = Gitlab::DataBuilder::Push.build_sample(fork_project, user)
+ create(:event, :pushed, project: fork_project, target: fork_project, author: user, data: push_data)
+ end
+
+ context 'when target project has enabled merge requests' do
+ it 'returns link to create merge request on source project' do
+ expect(url_params['merge_request[target_project_id]'].to_i).to eq(project.id)
+ end
+ end
+
+ context 'when target project has disabled merge requests' do
+ it 'returns link to create merge request on forked project' do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ expect(url_params['merge_request[target_project_id]'].to_i).to eq(fork_project.id)
+ end
+ end
+ end
+
describe '#mr_issues_mentioned_but_not_closing' do
let(:user_1) { create(:user) }
let(:user_2) { create(:user) }
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index a7fc5d14859..be97973c693 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -93,7 +93,7 @@ describe ProjectsHelper do
end
it "includes a version" do
- expect(helper.project_list_cache_key(project)).to include("v2.3")
+ expect(helper.project_list_cache_key(project).last).to start_with('v')
end
it "includes the pipeline status when there is a status" do
@@ -103,6 +103,18 @@ describe ProjectsHelper do
end
end
+ describe '#load_pipeline_status' do
+ it 'loads the pipeline status in batch' do
+ project = build(:empty_project)
+
+ helper.load_pipeline_status([project])
+ # Skip lazy loading of the `pipeline_status` attribute
+ pipeline_status = project.instance_variable_get('@pipeline_status')
+
+ expect(pipeline_status).to be_a(Gitlab::Cache::Ci::ProjectPipelineStatus)
+ end
+ end
+
describe 'link_to_member' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, group: group) }
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/blob/blob_fork_suggestion_spec.js b/spec/javascripts/blob/blob_fork_suggestion_spec.js
new file mode 100644
index 00000000000..d1ab0a32f85
--- /dev/null
+++ b/spec/javascripts/blob/blob_fork_suggestion_spec.js
@@ -0,0 +1,38 @@
+import BlobForkSuggestion from '~/blob/blob_fork_suggestion';
+
+describe('BlobForkSuggestion', () => {
+ let blobForkSuggestion;
+
+ const openButton = document.createElement('div');
+ const forkButton = document.createElement('a');
+ const cancelButton = document.createElement('div');
+ const suggestionSection = document.createElement('div');
+ const actionTextPiece = document.createElement('div');
+
+ beforeEach(() => {
+ blobForkSuggestion = new BlobForkSuggestion({
+ openButtons: openButton,
+ forkButtons: forkButton,
+ cancelButtons: cancelButton,
+ suggestionSections: suggestionSection,
+ actionTextPieces: actionTextPiece,
+ })
+ .init();
+ });
+
+ afterEach(() => {
+ blobForkSuggestion.destroy();
+ });
+
+ it('showSuggestionSection', () => {
+ blobForkSuggestion.showSuggestionSection('/foo', 'foo');
+ expect(suggestionSection.classList.contains('hidden')).toEqual(false);
+ expect(forkButton.getAttribute('href')).toEqual('/foo');
+ expect(actionTextPiece.textContent).toEqual('foo');
+ });
+
+ it('hideSuggestionSection', () => {
+ blobForkSuggestion.hideSuggestionSection();
+ expect(suggestionSection.classList.contains('hidden')).toEqual(true);
+ });
+});
diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js
index d3a4d04345b..bbeaf95e68d 100644
--- a/spec/javascripts/blob/pdf/index_spec.js
+++ b/spec/javascripts/blob/pdf/index_spec.js
@@ -1,5 +1,7 @@
+/* eslint-disable import/no-unresolved */
+
import renderPDF from '~/blob/pdf';
-import testPDF from './test.pdf';
+import testPDF from '../../fixtures/blob/pdf/test.pdf';
describe('PDF renderer', () => {
let viewer;
@@ -59,7 +61,7 @@ describe('PDF renderer', () => {
describe('error getting file', () => {
beforeEach((done) => {
- viewer.dataset.endpoint = 'invalid/endpoint';
+ viewer.dataset.endpoint = 'invalid/path/to/file.pdf';
app = renderPDF();
checkLoaded(done);
diff --git a/spec/javascripts/blob/pdf/test.pdf b/spec/javascripts/blob/pdf/test.pdf
deleted file mode 100644
index eb3d147fde3..00000000000
--- a/spec/javascripts/blob/pdf/test.pdf
+++ /dev/null
Binary files differ
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/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js
new file mode 100644
index 00000000000..13f122b68b2
--- /dev/null
+++ b/spec/javascripts/blob/viewer/index_spec.js
@@ -0,0 +1,161 @@
+/* eslint-disable no-new */
+import BlobViewer from '~/blob/viewer/index';
+
+describe('Blob viewer', () => {
+ let blob;
+ preloadFixtures('blob/show.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('blob/show.html.raw');
+ $('#modal-upload-blob').remove();
+
+ blob = new BlobViewer();
+
+ spyOn($, 'ajax').and.callFake(() => {
+ const d = $.Deferred();
+
+ d.resolve({
+ html: '<div>testing</div>',
+ });
+
+ return d.promise();
+ });
+ });
+
+ afterEach(() => {
+ location.hash = '';
+ });
+
+ it('loads source file after switching views', (done) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+ expect(
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
+ .classList.contains('hidden'),
+ ).toBeFalsy();
+
+ done();
+ });
+ });
+
+ it('loads source file when line number is in hash', (done) => {
+ location.hash = '#L1';
+
+ new BlobViewer();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+ expect(
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
+ .classList.contains('hidden'),
+ ).toBeFalsy();
+
+ done();
+ });
+ });
+
+ it('doesnt reload file if already loaded', (done) => {
+ const asyncClick = () => new Promise((resolve) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(resolve);
+ });
+
+ asyncClick()
+ .then(() => {
+ expect($.ajax).toHaveBeenCalled();
+ return asyncClick();
+ })
+ .then(() => {
+ expect($.ajax.calls.count()).toBe(1);
+ expect(
+ document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'),
+ ).toBe('true');
+
+ done();
+ })
+ .catch(() => {
+ fail();
+ done();
+ });
+ });
+
+ describe('copy blob button', () => {
+ it('disabled on load', () => {
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'),
+ ).toBeTruthy();
+ });
+
+ it('has tooltip when disabled', () => {
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'),
+ ).toBe('Switch to the source to copy it to the clipboard');
+ });
+
+ it('enables after switching to simple view', (done) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'),
+ ).toBeFalsy();
+
+ done();
+ });
+ });
+
+ it('updates tooltip after switching to simple view', (done) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'),
+ ).toBe('Copy source to clipboard');
+
+ done();
+ });
+ });
+ });
+
+ describe('switchToViewer', () => {
+ it('removes active class from old viewer button', () => {
+ blob.switchToViewer('simple');
+
+ expect(
+ document.querySelector('.js-blob-viewer-switch-btn.active[data-viewer="rich"]'),
+ ).toBeNull();
+ });
+
+ it('adds active class to new viewer button', () => {
+ const simpleBtn = document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]');
+
+ spyOn(simpleBtn, 'blur');
+
+ blob.switchToViewer('simple');
+
+ expect(
+ simpleBtn.classList.contains('active'),
+ ).toBeTruthy();
+ expect(simpleBtn.blur).toHaveBeenCalled();
+ });
+
+ it('sends AJAX request when switching to simple view', () => {
+ blob.switchToViewer('simple');
+
+ expect($.ajax).toHaveBeenCalled();
+ });
+
+ it('does not send AJAX request when switching to rich view', () => {
+ blob.switchToViewer('simple');
+ blob.switchToViewer('rich');
+
+ expect($.ajax.calls.count()).toBe(1);
+ });
+ });
+});
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 7174bf1e041..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`;
@@ -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 9762688af1a..1c54cc3054c 100644
--- a/spec/javascripts/environments/environment_spec.js
+++ b/spec/javascripts/environments/environment_spec.js
@@ -1,15 +1,18 @@
import Vue from 'vue';
import '~/flash';
-import EnvironmentsComponent from '~/environments/components/environment';
+import environmentsComponent from '~/environments/components/environment.vue';
import { environment, folder } from './mock_data';
describe('Environment', () => {
preloadFixtures('static/environments/environments.html.raw');
+ let EnvironmentsComponent;
let component;
beforeEach(() => {
loadFixtures('static/environments/environments.html.raw');
+
+ EnvironmentsComponent = Vue.extend(environmentsComponent);
});
describe('successfull request', () => {
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 72f3db29a66..350078ad5f5 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import '~/flash';
-import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view';
+import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import { environmentsList } from '../mock_data';
describe('Environments Folder View', () => {
preloadFixtures('static/environments/environments_folder_view.html.raw');
+ let EnvironmentsFolderViewComponent;
beforeEach(() => {
loadFixtures('static/environments/environments_folder_view.html.raw');
+ EnvironmentsFolderViewComponent = Vue.extend(environmentsFolderViewComponent);
window.history.pushState({}, null, 'environments/folders/build');
});
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..e747aa497c2 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -6,271 +6,324 @@ 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);
- }
-
- function dispatchDeleteEvent(element, eventType) {
- const deleteKey = 46;
- const event = new Event(eventType);
- event.keyCode = deleteKey;
- 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);
+ }
+
+ function getVisualTokens() {
+ return tokensContainer.querySelectorAll('.js-visual-token');
+ }
+
+ 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();
+ });
- 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>
- `);
+ afterEach(() => {
+ manager.cleanup();
+ });
- 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();
+ describe('search', () => {
+ const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
- input = document.querySelector('.filtered-search');
- tokensContainer = document.querySelector('.tokens-container');
- manager = new gl.FilteredSearchManager();
- });
+ it('should search with a single word', (done) => {
+ input.value = 'searchTerm';
- afterEach(() => {
- manager.cleanup();
- });
-
- describe('search', () => {
- const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&search=searchTerm`);
+ done();
+ });
- it('should search with a single word', (done) => {
- input.value = 'searchTerm';
+ manager.search();
+ });
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
- expect(url).toEqual(`${defaultParams}&search=searchTerm`);
- 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 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';
- it('should not render placeholder when there is input', () => {
- input.value = 'test words';
+ const event = new Event('input');
+ input.dispatchEvent(event);
- const event = new Event('input');
- input.dispatchEvent(event);
+ expect(input.placeholder).toEqual('');
+ });
- expect(input.placeholder).toEqual('');
- });
+ it('should not render placeholder when there are tokens and no input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ );
- it('should not render placeholder when there are tokens and no input', () => {
+ 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'),
);
-
- 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();
+
+ input.value = 'text';
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('text');
+ });
+ });
+
+ describe('removeToken', () => {
+ it('removes token even when it is already selected', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+ );
+ tokensContainer.querySelector('.js-visual-token .remove-token').click();
+ expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
+ });
+
+ describe('unselected token', () => {
beforeEach(() => {
+ spyOn(gl.FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough();
+
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'),
);
+ tokensContainer.querySelector('.js-visual-token .remove-token').click();
});
- it('removes selected token when the backspace key is pressed', () => {
- expect(getVisualTokens().length).toEqual(1);
-
- dispatchBackspaceEvent(document, 'keydown');
+ it('removes token when remove button is selected', () => {
+ expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
+ });
- expect(getVisualTokens().length).toEqual(0);
+ it('calls removeSelectedToken', () => {
+ expect(manager.removeSelectedToken).toHaveBeenCalled();
});
+ });
+ });
- it('removes selected token when the delete key is pressed', () => {
- expect(getVisualTokens().length).toEqual(1);
+ describe('removeSelectedTokenKeydown', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+ );
+ });
- dispatchDeleteEvent(document, 'keydown');
+ it('removes selected token when the backspace key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
- expect(getVisualTokens().length).toEqual(0);
- });
+ dispatchBackspaceEvent(document, 'keydown');
- it('updates the input placeholder after removal', () => {
- manager.handleInputPlaceholder();
+ expect(getVisualTokens().length).toEqual(0);
+ });
- expect(input.placeholder).toEqual('');
- expect(getVisualTokens().length).toEqual(1);
+ it('removes selected token when the delete key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
- dispatchBackspaceEvent(document, 'keydown');
+ dispatchDeleteEvent(document, 'keydown');
- expect(input.placeholder).not.toEqual('');
- expect(getVisualTokens().length).toEqual(0);
- });
+ expect(getVisualTokens().length).toEqual(0);
+ });
- it('updates the clear button after removal', () => {
- manager.toggleClearSearchButton();
+ it('updates the input placeholder after removal', () => {
+ manager.handleInputPlaceholder();
- const clearButton = document.querySelector('.clear-search');
+ expect(input.placeholder).toEqual('');
+ expect(getVisualTokens().length).toEqual(1);
- expect(clearButton.classList.contains('hidden')).toEqual(false);
- expect(getVisualTokens().length).toEqual(1);
+ dispatchBackspaceEvent(document, 'keydown');
- dispatchBackspaceEvent(document, 'keydown');
+ expect(input.placeholder).not.toEqual('');
+ expect(getVisualTokens().length).toEqual(0);
+ });
- expect(clearButton.classList.contains('hidden')).toEqual(true);
- expect(getVisualTokens().length).toEqual(0);
- });
+ it('updates the clear button after removal', () => {
+ manager.toggleClearSearchButton();
+
+ 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('removeSelectedToken', () => {
+ beforeEach(() => {
+ spyOn(gl.FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough();
+ spyOn(gl.FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough();
+ spyOn(gl.FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough();
+ manager.removeSelectedToken();
+ });
- it('unselects token when input is clicked', () => {
- const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+ it('calls FilteredSearchVisualTokens.removeSelectedToken', () => {
+ expect(gl.FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled();
+ });
- expect(selectedToken.classList.contains('selected')).toEqual(true);
- expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
+ it('calls handleInputPlaceholder', () => {
+ expect(manager.handleInputPlaceholder).toHaveBeenCalled();
+ });
- // Click directly on input attached to document
- // so that the click event will propagate properly
- document.querySelector('.filtered-search').click();
+ it('calls toggleClearSearchButton', () => {
+ expect(manager.toggleClearSearchButton).toHaveBeenCalled();
+ });
- expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
- expect(selectedToken.classList.contains('selected')).toEqual(false);
- });
+ it('calls update dropdown offset', () => {
+ expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled();
+ });
+ });
- it('unselects token when document.body is clicked', () => {
- const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+ describe('unselects token', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
+ `);
+ });
- expect(selectedToken.classList.contains('selected')).toEqual(true);
- expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
+ it('unselects token when input is clicked', () => {
+ const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
- document.body.click();
+ expect(selectedToken.classList.contains('selected')).toEqual(true);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
- expect(selectedToken.classList.contains('selected')).toEqual(false);
- expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
- });
+ // 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);
});
- describe('toggleInputContainerFocus', () => {
- it('toggles on focus', () => {
- input.focus();
- expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true);
- });
+ it('unselects token when document.body is clicked', () => {
+ const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
- it('toggles on blur', () => {
- input.blur();
- expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false);
- });
+ expect(selectedToken.classList.contains('selected')).toEqual(true);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
+
+ document.body.click();
+
+ 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);
+ });
+
+ 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/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
index bbda1476fed..d75b9061281 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -214,8 +214,12 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
});
+ it('contains value container div', () => {
+ expect(tokenElement.querySelector('.value-container')).toEqual(jasmine.anything());
+ });
+
it('contains value div', () => {
- expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything());
+ expect(tokenElement.querySelector('.value-container .value')).toEqual(jasmine.anything());
});
it('contains selectable class', () => {
@@ -225,6 +229,16 @@ describe('Filtered Search Visual Tokens', () => {
it('contains button role', () => {
expect(tokenElement.getAttribute('role')).toEqual('button');
});
+
+ describe('remove token', () => {
+ it('contains remove-token button', () => {
+ expect(tokenElement.querySelector('.value-container .remove-token')).toEqual(jasmine.anything());
+ });
+
+ it('contains fa-close icon', () => {
+ expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(jasmine.anything());
+ });
+ });
});
describe('addVisualTokenElement', () => {
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/blob.rb b/spec/javascripts/fixtures/blob.rb
new file mode 100644
index 00000000000..16490ad5039
--- /dev/null
+++ b/spec/javascripts/fixtures/blob.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('blob/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'blob/show.html.raw' do |example|
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: 'add-ipython-files/files/ipython/basic.ipynb')
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml
index 514877340e4..2782c50e298 100644
--- a/spec/javascripts/fixtures/line_highlighter.html.haml
+++ b/spec/javascripts/fixtures/line_highlighter.html.haml
@@ -1,4 +1,4 @@
-#blob-content-holder
+.file-holder
.file-content
.line-numbers
- 1.upto(25) do |i|
diff --git a/spec/javascripts/fixtures/pdf.rb b/spec/javascripts/fixtures/pdf.rb
new file mode 100644
index 00000000000..6b2422a7986
--- /dev/null
+++ b/spec/javascripts/fixtures/pdf.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe 'PDF file', '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'pdf-project') }
+
+ before(:all) do
+ clean_frontend_fixtures('blob/pdf/')
+ end
+
+ it 'blob/pdf/test.pdf' do |example|
+ blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf')
+
+ store_frontend_fixture(blob.data.force_encoding("utf-8"), example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/raw.rb b/spec/javascripts/fixtures/raw.rb
new file mode 100644
index 00000000000..1ce622fc836
--- /dev/null
+++ b/spec/javascripts/fixtures/raw.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe 'Raw files', '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'raw-project') }
+
+ before(:all) do
+ clean_frontend_fixtures('blob/notebook/')
+ end
+
+ it 'blob/notebook/basic.json' do |example|
+ blob = project.repository.blob_at('6d85bb69', 'files/ipython/basic.ipynb')
+
+ store_frontend_fixture(blob.data, example.description)
+ end
+
+ it 'blob/notebook/worksheets.json' do |example|
+ blob = project.repository.blob_at('6d85bb69', 'files/ipython/worksheets.ipynb')
+
+ store_frontend_fixture(blob.data, example.description)
+ end
+end
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js
index ce83a256ddd..b8d4a93b1ab 100644
--- a/spec/javascripts/helpers/filtered_search_spec_helper.js
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js
@@ -10,7 +10,12 @@ class FilteredSearchSpecHelper {
li.innerHTML = `
<div class="selectable ${isSelected ? 'selected' : ''}" role="button">
<div class="name">${name}</div>
- <div class="value">${value}</div>
+ <div class="value-container">
+ <div class="value">${value}</div>
+ <div class="remove-token" role="button">
+ <i class="fa fa-close"></i>
+ </div>
+ </div>
</div>
`;
diff --git a/spec/javascripts/landing_spec.js b/spec/javascripts/landing_spec.js
new file mode 100644
index 00000000000..7916073190a
--- /dev/null
+++ b/spec/javascripts/landing_spec.js
@@ -0,0 +1,160 @@
+import Landing from '~/landing';
+import Cookies from 'js-cookie';
+
+describe('Landing', function () {
+ describe('class constructor', function () {
+ beforeEach(function () {
+ this.landingElement = {};
+ this.dismissButton = {};
+ this.cookieName = 'cookie_name';
+
+ this.landing = new Landing(this.landingElement, this.dismissButton, this.cookieName);
+ });
+
+ it('should set .landing', function () {
+ expect(this.landing.landingElement).toBe(this.landingElement);
+ });
+
+ it('should set .cookieName', function () {
+ expect(this.landing.cookieName).toBe(this.cookieName);
+ });
+
+ it('should set .dismissButton', function () {
+ expect(this.landing.dismissButton).toBe(this.dismissButton);
+ });
+
+ it('should set .eventWrapper', function () {
+ expect(this.landing.eventWrapper).toEqual({});
+ });
+ });
+
+ describe('toggle', function () {
+ beforeEach(function () {
+ this.isDismissed = false;
+ this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
+ this.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: this.landingElement,
+ };
+
+ spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
+ spyOn(this.landing, 'addEvents');
+
+ Landing.prototype.toggle.call(this.landing);
+ });
+
+ it('should call .isDismissed', function () {
+ expect(this.landing.isDismissed).toHaveBeenCalled();
+ });
+
+ it('should call .classList.toggle', function () {
+ expect(this.landingElement.classList.toggle).toHaveBeenCalledWith('hidden', this.isDismissed);
+ });
+
+ it('should call .addEvents', function () {
+ expect(this.landing.addEvents).toHaveBeenCalled();
+ });
+
+ describe('if isDismissed is true', function () {
+ beforeEach(function () {
+ this.isDismissed = true;
+ this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
+ this.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: this.landingElement,
+ };
+
+ spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
+ spyOn(this.landing, 'addEvents');
+
+ this.landing.isDismissed.calls.reset();
+
+ Landing.prototype.toggle.call(this.landing);
+ });
+
+ it('should not call .addEvents', function () {
+ expect(this.landing.addEvents).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addEvents', function () {
+ beforeEach(function () {
+ this.dismissButton = jasmine.createSpyObj('dismissButton', ['addEventListener']);
+ this.eventWrapper = {};
+ this.landing = {
+ eventWrapper: this.eventWrapper,
+ dismissButton: this.dismissButton,
+ dismissLanding: () => {},
+ };
+
+ Landing.prototype.addEvents.call(this.landing);
+ });
+
+ it('should set .eventWrapper.dismissLanding', function () {
+ expect(this.eventWrapper.dismissLanding).toEqual(jasmine.any(Function));
+ });
+
+ it('should call .addEventListener', function () {
+ expect(this.dismissButton.addEventListener).toHaveBeenCalledWith('click', this.eventWrapper.dismissLanding);
+ });
+ });
+
+ describe('removeEvents', function () {
+ beforeEach(function () {
+ this.dismissButton = jasmine.createSpyObj('dismissButton', ['removeEventListener']);
+ this.eventWrapper = { dismissLanding: () => {} };
+ this.landing = {
+ eventWrapper: this.eventWrapper,
+ dismissButton: this.dismissButton,
+ };
+
+ Landing.prototype.removeEvents.call(this.landing);
+ });
+
+ it('should call .removeEventListener', function () {
+ expect(this.dismissButton.removeEventListener).toHaveBeenCalledWith('click', this.eventWrapper.dismissLanding);
+ });
+ });
+
+ describe('dismissLanding', function () {
+ beforeEach(function () {
+ this.landingElement = { classList: jasmine.createSpyObj('classList', ['add']) };
+ this.cookieName = 'cookie_name';
+ this.landing = { landingElement: this.landingElement, cookieName: this.cookieName };
+
+ spyOn(Cookies, 'set');
+
+ Landing.prototype.dismissLanding.call(this.landing);
+ });
+
+ it('should call .classList.add', function () {
+ expect(this.landingElement.classList.add).toHaveBeenCalledWith('hidden');
+ });
+
+ it('should call Cookies.set', function () {
+ expect(Cookies.set).toHaveBeenCalledWith(this.cookieName, 'true', { expires: 365 });
+ });
+ });
+
+ describe('isDismissed', function () {
+ beforeEach(function () {
+ this.cookieName = 'cookie_name';
+ this.landing = { cookieName: this.cookieName };
+
+ spyOn(Cookies, 'get').and.returnValue('true');
+
+ this.isDismissed = Landing.prototype.isDismissed.call(this.landing);
+ });
+
+ it('should call Cookies.get', function () {
+ expect(Cookies.get).toHaveBeenCalledWith(this.cookieName);
+ });
+
+ it('should return a boolean', function () {
+ expect(typeof this.isDismissed).toEqual('boolean');
+ });
+ });
+});
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/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
index e504d41d4d4..481b46c3ac6 100644
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
@@ -3,70 +3,84 @@
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import '~/flash';
-(() => {
- describe('Mini Pipeline Graph Dropdown', () => {
- preloadFixtures('static/mini_dropdown_graph.html.raw');
+describe('Mini Pipeline Graph Dropdown', () => {
+ preloadFixtures('static/mini_dropdown_graph.html.raw');
- beforeEach(() => {
- loadFixtures('static/mini_dropdown_graph.html.raw');
- });
+ beforeEach(() => {
+ loadFixtures('static/mini_dropdown_graph.html.raw');
+ });
- describe('When is initialized', () => {
- it('should initialize without errors when no options are given', () => {
- const miniPipelineGraph = new MiniPipelineGraph();
+ describe('When is initialized', () => {
+ it('should initialize without errors when no options are given', () => {
+ const miniPipelineGraph = new MiniPipelineGraph();
- expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
- });
+ expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
+ });
- it('should set the container as the given prop', () => {
- const container = '.foo';
+ it('should set the container as the given prop', () => {
+ const container = '.foo';
- const miniPipelineGraph = new MiniPipelineGraph({ container });
+ const miniPipelineGraph = new MiniPipelineGraph({ container });
- expect(miniPipelineGraph.container).toEqual(container);
- });
+ expect(miniPipelineGraph.container).toEqual(container);
});
+ });
- describe('When dropdown is clicked', () => {
- it('should call getBuildsList', () => {
- const getBuildsListSpy = spyOn(MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
+ describe('When dropdown is clicked', () => {
+ it('should call getBuildsList', () => {
+ const getBuildsListSpy = spyOn(
+ MiniPipelineGraph.prototype,
+ 'getBuildsList',
+ ).and.callFake(function () {});
- new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
- document.querySelector('.js-builds-dropdown-button').click();
+ document.querySelector('.js-builds-dropdown-button').click();
- expect(getBuildsListSpy).toHaveBeenCalled();
- });
+ expect(getBuildsListSpy).toHaveBeenCalled();
+ });
- it('should make a request to the endpoint provided in the html', () => {
- const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
+ it('should make a request to the endpoint provided in the html', () => {
+ const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
- new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
- document.querySelector('.js-builds-dropdown-button').click();
- expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
- });
+ document.querySelector('.js-builds-dropdown-button').click();
+ expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
+ });
- it('should not close when user uses cmd/ctrl + click', () => {
- spyOn($, 'ajax').and.callFake(function (params) {
- params.success({
- html: `<li>
- <a class="mini-pipeline-graph-dropdown-item" href="#">
- <span class="ci-status-icon ci-status-icon-failed"></span>
- <span class="ci-build-text">build</span>
- </a>
- <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
- </li>`,
- });
+ it('should not close when user uses cmd/ctrl + click', () => {
+ spyOn($, 'ajax').and.callFake(function (params) {
+ params.success({
+ html: `<li>
+ <a class="mini-pipeline-graph-dropdown-item" href="#">
+ <span class="ci-status-icon ci-status-icon-failed"></span>
+ <span class="ci-build-text">build</span>
+ </a>
+ <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
+ </li>`,
});
- new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+ });
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
- document.querySelector('.js-builds-dropdown-button').click();
+ document.querySelector('.js-builds-dropdown-button').click();
- document.querySelector('a.mini-pipeline-graph-dropdown-item').click();
+ document.querySelector('a.mini-pipeline-graph-dropdown-item').click();
- expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
- });
+ expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
});
});
-})();
+
+ it('should close the dropdown when request returns an error', (done) => {
+ spyOn($, 'ajax').and.callFake(options => options.error());
+
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+
+ document.querySelector('.js-builds-dropdown-button').click();
+
+ setTimeout(() => {
+ expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false);
+ done();
+ }, 0);
+ });
+});
diff --git a/spec/javascripts/notebook/cells/code_spec.js b/spec/javascripts/notebook/cells/code_spec.js
new file mode 100644
index 00000000000..0c432d73f67
--- /dev/null
+++ b/spec/javascripts/notebook/cells/code_spec.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import CodeComponent from '~/notebook/cells/code.vue';
+
+const Component = Vue.extend(CodeComponent);
+
+describe('Code component', () => {
+ let vm;
+ let json;
+
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/basic.json');
+ });
+
+ describe('without output', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ cell: json.cells[0],
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('does not render output prompt', () => {
+ expect(vm.$el.querySelectorAll('.prompt').length).toBe(1);
+ });
+ });
+
+ describe('with output', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ cell: json.cells[2],
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('does not render output prompt', () => {
+ expect(vm.$el.querySelectorAll('.prompt').length).toBe(2);
+ });
+
+ it('renders output cell', () => {
+ expect(vm.$el.querySelector('.output')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js
new file mode 100644
index 00000000000..38c976f38d8
--- /dev/null
+++ b/spec/javascripts/notebook/cells/markdown_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import MarkdownComponent from '~/notebook/cells/markdown.vue';
+
+const Component = Vue.extend(MarkdownComponent);
+
+describe('Markdown component', () => {
+ let vm;
+ let cell;
+ let json;
+
+ beforeEach((done) => {
+ json = getJSONFixture('blob/notebook/basic.json');
+
+ cell = json.cells[1];
+
+ vm = new Component({
+ propsData: {
+ cell,
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('does not render promot', () => {
+ expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ });
+
+ it('does not render the markdown text', () => {
+ expect(
+ vm.$el.querySelector('.markdown').innerHTML.trim(),
+ ).not.toEqual(cell.source.join(''));
+ });
+
+ it('renders the markdown HTML', () => {
+ expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
+ });
+});
diff --git a/spec/javascripts/notebook/cells/output/index_spec.js b/spec/javascripts/notebook/cells/output/index_spec.js
new file mode 100644
index 00000000000..dbf79f85c7c
--- /dev/null
+++ b/spec/javascripts/notebook/cells/output/index_spec.js
@@ -0,0 +1,126 @@
+import Vue from 'vue';
+import CodeComponent from '~/notebook/cells/output/index.vue';
+
+const Component = Vue.extend(CodeComponent);
+
+describe('Output component', () => {
+ let vm;
+ let json;
+
+ const createComponent = (output) => {
+ vm = new Component({
+ propsData: {
+ output,
+ count: 1,
+ },
+ });
+ vm.$mount();
+ };
+
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/basic.json');
+ });
+
+ describe('text output', () => {
+ beforeEach((done) => {
+ createComponent(json.cells[2].outputs[0]);
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders as plain text', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ });
+
+ it('renders promot', () => {
+ expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
+ });
+ });
+
+ describe('image output', () => {
+ beforeEach((done) => {
+ createComponent(json.cells[3].outputs[0]);
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders as an image', () => {
+ expect(vm.$el.querySelector('img')).not.toBeNull();
+ });
+
+ it('does not render the prompt', () => {
+ expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ });
+ });
+
+ describe('html output', () => {
+ beforeEach((done) => {
+ createComponent(json.cells[4].outputs[0]);
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders raw HTML', () => {
+ expect(vm.$el.querySelector('p')).not.toBeNull();
+ expect(vm.$el.textContent.trim()).toBe('test');
+ });
+
+ it('does not render the prompt', () => {
+ expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ });
+ });
+
+ describe('svg output', () => {
+ beforeEach((done) => {
+ createComponent(json.cells[5].outputs[0]);
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders as an svg', () => {
+ expect(vm.$el.querySelector('svg')).not.toBeNull();
+ });
+
+ it('does not render the prompt', () => {
+ expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ });
+ });
+
+ describe('default to plain text', () => {
+ beforeEach((done) => {
+ createComponent(json.cells[6].outputs[0]);
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders as plain text', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ expect(vm.$el.textContent.trim()).toContain('testing');
+ });
+
+ it('renders promot', () => {
+ expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
+ });
+
+ it('renders as plain text when doesn\'t recognise other types', (done) => {
+ createComponent(json.cells[7].outputs[0]);
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ expect(vm.$el.textContent.trim()).toContain('testing');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/notebook/cells/prompt_spec.js b/spec/javascripts/notebook/cells/prompt_spec.js
new file mode 100644
index 00000000000..207fa433a59
--- /dev/null
+++ b/spec/javascripts/notebook/cells/prompt_spec.js
@@ -0,0 +1,56 @@
+import Vue from 'vue';
+import PromptComponent from '~/notebook/cells/prompt.vue';
+
+const Component = Vue.extend(PromptComponent);
+
+describe('Prompt component', () => {
+ let vm;
+
+ describe('input', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ type: 'In',
+ count: 1,
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders in label', () => {
+ expect(vm.$el.textContent.trim()).toContain('In');
+ });
+
+ it('renders count', () => {
+ expect(vm.$el.textContent.trim()).toContain('1');
+ });
+ });
+
+ describe('output', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ type: 'Out',
+ count: 1,
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders in label', () => {
+ expect(vm.$el.textContent.trim()).toContain('Out');
+ });
+
+ it('renders count', () => {
+ expect(vm.$el.textContent.trim()).toContain('1');
+ });
+ });
+});
diff --git a/spec/javascripts/notebook/index_spec.js b/spec/javascripts/notebook/index_spec.js
new file mode 100644
index 00000000000..bd63ab35426
--- /dev/null
+++ b/spec/javascripts/notebook/index_spec.js
@@ -0,0 +1,98 @@
+import Vue from 'vue';
+import Notebook from '~/notebook/index.vue';
+
+const Component = Vue.extend(Notebook);
+
+describe('Notebook component', () => {
+ let vm;
+ let json;
+ let jsonWithWorksheet;
+
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/basic.json');
+ jsonWithWorksheet = getJSONFixture('blob/notebook/worksheets.json');
+ });
+
+ describe('without JSON', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ notebook: {},
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('does not render', () => {
+ expect(vm.$el.tagName).toBeUndefined();
+ });
+ });
+
+ describe('with JSON', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ notebook: json,
+ codeCssClass: 'js-code-class',
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders cells', () => {
+ expect(vm.$el.querySelectorAll('.cell').length).toBe(json.cells.length);
+ });
+
+ it('renders markdown cell', () => {
+ expect(vm.$el.querySelector('.markdown')).not.toBeNull();
+ });
+
+ it('renders code cell', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ });
+
+ it('add code class to code blocks', () => {
+ expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
+ });
+ });
+
+ describe('with worksheets', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ notebook: jsonWithWorksheet,
+ codeCssClass: 'js-code-class',
+ },
+ });
+ vm.$mount();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('renders cells', () => {
+ expect(vm.$el.querySelectorAll('.cell').length).toBe(jsonWithWorksheet.worksheets[0].cells.length);
+ });
+
+ it('renders markdown cell', () => {
+ expect(vm.$el.querySelector('.markdown')).not.toBeNull();
+ });
+
+ it('renders code cell', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ });
+
+ it('add code class to code blocks', () => {
+ expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/notebook/lib/highlight_spec.js b/spec/javascripts/notebook/lib/highlight_spec.js
new file mode 100644
index 00000000000..d71c5718858
--- /dev/null
+++ b/spec/javascripts/notebook/lib/highlight_spec.js
@@ -0,0 +1,15 @@
+import Prism from '~/notebook/lib/highlight';
+
+describe('Highlight library', () => {
+ it('imports python language', () => {
+ expect(Prism.languages.python).toBeDefined();
+ });
+
+ it('uses custom CSS classes', () => {
+ const el = document.createElement('div');
+ el.innerHTML = Prism.highlight('console.log("a");', Prism.languages.javascript);
+
+ expect(el.querySelector('.s')).not.toBeNull();
+ expect(el.querySelector('.nf')).not.toBeNull();
+ });
+});
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/pdf/index_spec.js b/spec/javascripts/pdf/index_spec.js
new file mode 100644
index 00000000000..f661fae5fe2
--- /dev/null
+++ b/spec/javascripts/pdf/index_spec.js
@@ -0,0 +1,61 @@
+/* eslint-disable import/no-unresolved */
+
+import Vue from 'vue';
+import { PDFJS } from 'pdfjs-dist';
+import workerSrc from 'vendor/pdf.worker';
+
+import PDFLab from '~/pdf/index.vue';
+import pdf from '../fixtures/blob/pdf/test.pdf';
+
+PDFJS.workerSrc = workerSrc;
+const Component = Vue.extend(PDFLab);
+
+describe('PDF component', () => {
+ let vm;
+
+ const checkLoaded = (done) => {
+ if (vm.loading) {
+ setTimeout(() => {
+ checkLoaded(done);
+ }, 100);
+ } else {
+ done();
+ }
+ };
+
+ describe('without PDF data', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ pdf: '',
+ },
+ });
+
+ vm.$mount();
+
+ checkLoaded(done);
+ });
+
+ it('does not render', () => {
+ expect(vm.$el.tagName).toBeUndefined();
+ });
+ });
+
+ describe('with PDF data', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ pdf,
+ },
+ });
+
+ vm.$mount();
+
+ checkLoaded(done);
+ });
+
+ it('renders pdf component', () => {
+ expect(vm.$el.tagName).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js
new file mode 100644
index 00000000000..ac76ebbfbe6
--- /dev/null
+++ b/spec/javascripts/pdf/page_spec.js
@@ -0,0 +1,57 @@
+/* eslint-disable import/no-unresolved */
+
+import Vue from 'vue';
+import pdfjsLib from 'pdfjs-dist';
+import workerSrc from 'vendor/pdf.worker';
+
+import PageComponent from '~/pdf/page/index.vue';
+import testPDF from '../fixtures/blob/pdf/test.pdf';
+
+const Component = Vue.extend(PageComponent);
+
+describe('Page component', () => {
+ let vm;
+ let testPage;
+ pdfjsLib.PDFJS.workerSrc = workerSrc;
+
+ const checkRendered = (done) => {
+ if (vm.rendering) {
+ setTimeout(() => {
+ checkRendered(done);
+ }, 100);
+ } else {
+ done();
+ }
+ };
+
+ beforeEach((done) => {
+ pdfjsLib.getDocument(testPDF)
+ .then(pdf => pdf.getPage(1))
+ .then((page) => {
+ testPage = page;
+ done();
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ });
+
+ describe('render', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ page: testPage,
+ number: 1,
+ },
+ });
+
+ vm.$mount();
+
+ checkRendered(done);
+ });
+
+ it('renders first page', () => {
+ expect(vm.$el.tagName).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js
index 6e910d2dc71..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.vue';
+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 2b10d54babe..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.vue';
+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 7999c15c18d..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.vue';
+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..2f1154bd999
--- /dev/null
+++ b/spec/javascripts/pipelines/stage_spec.js
@@ -0,0 +1,81 @@
+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);
+ });
+ });
+
+ describe('when request fails', () => {
+ it('closes dropdown', () => {
+ spyOn($, 'ajax').and.callFake(options => options.error());
+ const StageComponent = Vue.extend(Stage);
+
+ const component = new StageComponent({
+ propsData: { stage: { status: { icon: 'foo' } } },
+ }).$mount();
+
+ expect(
+ component.$el.classList.contains('open'),
+ ).toEqual(false);
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/time_ago_spec.js b/spec/javascripts/pipelines/time_ago_spec.js
new file mode 100644
index 00000000000..24581e8c672
--- /dev/null
+++ b/spec/javascripts/pipelines/time_ago_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import timeAgo from '~/pipelines/components/time_ago';
+
+describe('Timeago component', () => {
+ let TimeAgo;
+ beforeEach(() => {
+ TimeAgo = Vue.extend(timeAgo);
+ });
+
+ describe('with duration', () => {
+ it('should render duration and timer svg', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 10,
+ finishedTime: '',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.duration')).toBeDefined();
+ expect(component.$el.querySelector('.duration svg')).toBeDefined();
+ });
+ });
+
+ describe('without duration', () => {
+ it('should not render duration and timer svg', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 0,
+ finishedTime: '',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.duration')).toBe(null);
+ });
+ });
+
+ describe('with finishedTime', () => {
+ it('should render time and calendar icon', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 0,
+ finishedTime: '2017-04-26T12:40:23.277Z',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.finished-at')).toBeDefined();
+ expect(component.$el.querySelector('.finished-at i.fa-calendar')).toBeDefined();
+ expect(component.$el.querySelector('.finished-at time')).toBeDefined();
+ });
+ });
+
+ describe('without finishedTime', () => {
+ it('should not render time and calendar icon', () => {
+ const component = new TimeAgo({
+ propsData: {
+ duration: 0,
+ finishedTime: '',
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.finished-at')).toBe(null);
+ });
+ });
+});
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/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index 707212e07fd..086a006c45f 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -68,9 +68,9 @@ describe Banzai::Filter::EmojiFilter, lib: true do
expect(doc.css('gl-emoji').size).to eq 1
end
- it 'matches multiple emoji in a row' do
+ it 'does not match multiple emoji in a row' do
doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
- expect(doc.css('gl-emoji').size).to eq 3
+ expect(doc.css('gl-emoji').size).to eq 0
end
it 'unicode matches multiple emoji in a row' do
@@ -83,6 +83,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do
expect(doc.css('gl-emoji').size).to eq 6
end
+ it 'does not match emoji in a string' do
+ doc = filter("'2a00:a4c0:100::1'")
+
+ expect(doc.css('gl-emoji').size).to eq 0
+ end
+
it 'has a data-name attribute' do
doc = filter(':-1:')
expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown'
diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
index 5cb98163746..9c2399815b9 100644
--- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb
+++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
@@ -5,11 +5,24 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
include FilterSpecHelper
let(:user) { create(:user) }
+ let(:context) { { current_user: user, issuable_state_filter_enabled: true } }
+ let(:closed_issue) { create_issue(:closed) }
+ let(:project) { create(:empty_project, :public) }
+ let(:other_project) { create(:empty_project, :public) }
def create_link(text, data)
link_to(text, '', class: 'gfm has-tooltip', data: data)
end
+ def create_issue(state)
+ create(:issue, state, project: project)
+ end
+
+ def create_merge_request(state)
+ create(:merge_request, state,
+ source_project: project, target_project: project)
+ end
+
it 'ignores non-GFM links' do
html = %(See <a href="https://google.com/">Google</a>)
doc = filter(html, current_user: user)
@@ -18,99 +31,167 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
end
it 'ignores non-issuable links' do
- project = create(:empty_project, :public)
link = create_link('text', project: project, reference_type: 'issue')
- doc = filter(link, current_user: user)
+ 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, current_user: user)
+ link = create_link('', issue: closed_issue.id, reference_type: 'issue')
+ doc = filter(link, context)
expect(doc.css('a').last.text).to eq('')
end
- it 'adds text with standard formatting' do
- issue = create(:issue, :closed)
+ it 'ignores issuable links with custom anchor' do
+ link = create_link('something', issue: closed_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
+ link = create_link("#{closed_issue.to_reference} (comment 1)", issue: closed_issue.id, reference_type: 'issue')
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference} (comment 1)")
+ end
+
+ it 'ignores merge request links to diffs tab' do
+ merge_request = create(:merge_request, :closed)
link = create_link(
- 'something <strong>else</strong>'.html_safe,
- issue: issue.id,
- reference_type: 'issue'
+ "#{merge_request.to_reference} (diffs)",
+ merge_request: merge_request.id,
+ reference_type: 'merge_request'
)
- doc = filter(link, current_user: user)
+ doc = filter(link, context)
- expect(doc.css('a').last.inner_html).
- to eq('something <strong>else</strong> [closed]')
+ expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (diffs)")
+ end
+
+ it 'handles cross project references' do
+ link = create_link(closed_issue.to_reference(other_project), issue: closed_issue.id, reference_type: 'issue')
+ doc = filter(link, context.merge(project: other_project))
+
+ expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)} (closed)")
+ end
+
+ it 'does not append state when filter is not enabled' do
+ link = create_link('text', issue: closed_issue.id, reference_type: 'issue')
+ context = { current_user: user }
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq('text')
+ end
+
+ context 'when project is in pending delete' do
+ before do
+ project.update!(pending_delete: true)
+ end
+
+ it 'does not append issue state' do
+ link = create_link('text', issue: closed_issue.id, reference_type: 'issue')
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq('text')
+ end
end
context 'for issue references' do
it 'ignores open issue references' do
- issue = create(:issue)
- link = create_link('text', issue: issue.id, reference_type: 'issue')
- doc = filter(link, current_user: user)
+ issue = create_issue(:opened)
+ 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('text', 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('text', issue: closed_issue.id, reference_type: 'issue')
- doc = filter(link, current_user: user)
+ it 'appends state to closed issue references' do
+ link = create_link(closed_issue.to_reference, issue: closed_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("#{closed_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('text', merge_request: mr.id, reference_type: 'merge_request')
- doc = filter(link, current_user: user)
+ merge_request = create_merge_request(:opened)
- expect(doc.css('a').last.text).to eq('text')
+ 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('text', merge_request: mr.id, reference_type: 'merge_request')
- doc = filter(link, current_user: user)
+ merge_request = create_merge_request(:reopened)
- expect(doc.css('a').last.text).to eq('text')
+ 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('text', merge_request: mr.id, reference_type: 'merge_request')
- doc = filter(link, current_user: user)
+ merge_request = create_merge_request(:locked)
- expect(doc.css('a').last.text).to eq('text')
+ 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('text', 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('text', 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/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 f3b3a9a715f..c2bcb54210b 100644
--- a/spec/lib/container_registry/path_spec.rb
+++ b/spec/lib/container_registry/path_spec.rb
@@ -189,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
@@ -225,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/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index bca57105d1d..0f47fb2fbd9 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -22,26 +22,9 @@ module Gitlab
expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html)
- expect( render(input, context) ).to eql html
+ expect(render(input)).to eq(html)
end
- context "with asciidoc_opts" do
- let(:asciidoc_opts) { { safe: :safe, attributes: ['foo'] } }
-
- it "merges the options with default ones" do
- expected_asciidoc_opts = {
- safe: :safe,
- backend: :gitlab_html5,
- attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo']
- }
-
- expect(Asciidoctor).to receive(:convert)
- .with(input, expected_asciidoc_opts).and_return(html)
-
- render(input, context, asciidoc_opts)
- end
- end
-
context "XSS" do
links = {
'links' => {
@@ -60,7 +43,7 @@ module Gitlab
links.each do |name, data|
it "does not convert dangerous #{name} into HTML" do
- expect(render(data[:input], context)).to eql data[:output]
+ expect(render(data[:input])).to eq(data[:output])
end
end
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 03c4879ed6f..d4a43192d03 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -118,7 +118,7 @@ describe Gitlab::Auth, lib: true do
it 'succeeds for OAuth tokens with the `api` scope' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2')
- expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
+ expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities))
end
it 'fails for OAuth tokens with other scopes' do
diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
index fced253dd01..b386852b196 100644
--- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
+++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
-describe Gitlab::Cache::Ci::ProjectPipelineStatus do
+describe Gitlab::Cache::Ci::ProjectPipelineStatus, :redis do
let(:project) { create(:project) }
let(:pipeline_status) { described_class.new(project) }
+ let(:cache_key) { "projects/#{project.id}/pipeline_status" }
describe '.load_for_project' do
it "loads the status" do
@@ -12,12 +13,110 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
end
end
+ describe 'loading in batches' do
+ let(:status) { 'success' }
+ let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
+ let(:ref) { 'master' }
+ let(:pipeline_info) { { sha: sha, status: status, ref: ref } }
+ let(:project_without_status) { create(:project) }
+
+ describe '.load_in_batch_for_projects' do
+ it 'preloads pipeline_status on projects' do
+ described_class.load_in_batch_for_projects([project])
+
+ # Don't call the accessor that would lazy load the variable
+ expect(project.instance_variable_get('@pipeline_status')).to be_a(described_class)
+ end
+
+ describe 'without a status in redis' do
+ it 'loads the status from a commit when it was not in redis' do
+ empty_status = { sha: nil, status: nil, ref: nil }
+ fake_pipeline = described_class.new(
+ project_without_status,
+ pipeline_info: empty_status,
+ loaded_from_cache: false
+ )
+
+ expect(described_class).to receive(:new).
+ with(project_without_status,
+ pipeline_info: empty_status,
+ loaded_from_cache: false).
+ and_return(fake_pipeline)
+ expect(fake_pipeline).to receive(:load_from_project)
+ expect(fake_pipeline).to receive(:store_in_cache)
+
+ described_class.load_in_batch_for_projects([project_without_status])
+ end
+
+ it 'only connects to redis twice' do
+ # Once to load, once to store in the cache
+ expect(Gitlab::Redis).to receive(:with).exactly(2).and_call_original
+
+ described_class.load_in_batch_for_projects([project_without_status])
+
+ expect(project_without_status.pipeline_status).not_to be_nil
+ end
+ end
+
+ describe 'when a status was cached in redis' do
+ before do
+ Gitlab::Redis.with do |redis|
+ redis.mapped_hmset(cache_key,
+ { sha: sha, status: status, ref: ref })
+ end
+ end
+
+ it 'loads the correct status' do
+ described_class.load_in_batch_for_projects([project])
+
+ pipeline_status = project.instance_variable_get('@pipeline_status')
+
+ expect(pipeline_status.sha).to eq(sha)
+ expect(pipeline_status.status).to eq(status)
+ expect(pipeline_status.ref).to eq(ref)
+ end
+
+ it 'only connects to redis once' do
+ expect(Gitlab::Redis).to receive(:with).exactly(1).and_call_original
+
+ described_class.load_in_batch_for_projects([project])
+
+ expect(project.pipeline_status).not_to be_nil
+ end
+
+ it "doesn't load the status separatly" do
+ expect_any_instance_of(described_class).not_to receive(:load_from_project)
+ expect_any_instance_of(described_class).not_to receive(:load_from_cache)
+
+ described_class.load_in_batch_for_projects([project])
+ end
+ end
+ end
+
+ describe '.cached_results_for_projects' do
+ it 'loads a status from redis for all projects' do
+ Gitlab::Redis.with do |redis|
+ redis.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref })
+ end
+
+ result = [{ loaded_from_cache: false, pipeline_info: { sha: nil, status: nil, ref: nil } },
+ { loaded_from_cache: true, pipeline_info: pipeline_info }]
+
+ expect(described_class.cached_results_for_projects([project_without_status, project])).to eq(result)
+ end
+ end
+ end
+
describe '.update_for_pipeline' do
it 'refreshes the cache if nescessary' do
- pipeline = build_stubbed(:ci_pipeline, sha: '123456', status: 'success')
+ pipeline = build_stubbed(:ci_pipeline,
+ sha: '123456', status: 'success', ref: 'master')
fake_status = double
expect(described_class).to receive(:new).
- with(pipeline.project, sha: '123456', status: 'success', ref: 'master').
+ with(pipeline.project,
+ pipeline_info: {
+ sha: '123456', status: 'success', ref: 'master'
+ }).
and_return(fake_status)
expect(fake_status).to receive(:store_in_cache_if_needed)
@@ -110,7 +209,7 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
pipeline_status.status = 'failed'
pipeline_status.store_in_cache
- read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+ read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status) }
expect(read_sha).to eq('123456')
expect(read_status).to eq('failed')
@@ -120,10 +219,10 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
describe '#store_in_cache_if_needed', :redis do
it 'stores the state in the cache when the sha is the HEAD of the project' do
create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
- build_status = described_class.load_for_project(project)
+ pipeline_status = described_class.load_for_project(project)
- build_status.store_in_cache_if_needed
- sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status, :ref) }
+ pipeline_status.store_in_cache_if_needed
+ sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status, :ref) }
expect(sha).not_to be_nil
expect(status).not_to be_nil
@@ -131,10 +230,13 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
end
it "doesn't store the status in redis when the sha is not the head of the project" do
- other_status = described_class.new(project, sha: "123456", status: "failed")
+ other_status = described_class.new(
+ project,
+ pipeline_info: { sha: "123456", status: "failed" }
+ )
other_status.store_in_cache_if_needed
- sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+ sha, status = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status) }
expect(sha).to be_nil
expect(status).to be_nil
@@ -142,11 +244,18 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
it "deletes the cache if the repository doesn't have a head commit" do
empty_project = create(:empty_project)
- Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending", ref: 'master' }) }
- other_status = described_class.new(empty_project, sha: "123456", status: "failed")
+ Gitlab::Redis.with do |redis|
+ redis.mapped_hmset(cache_key,
+ { sha: 'sha', status: 'pending', ref: 'master' })
+ end
+
+ other_status = described_class.new(empty_project,
+ pipeline_info: {
+ sha: "123456", status: "failed"
+ })
other_status.store_in_cache_if_needed
- sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status, :ref) }
+ sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/pipeline_status", :sha, :status, :ref) }
expect(sha).to be_nil
expect(status).to be_nil
@@ -157,9 +266,13 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
describe "with a status in redis", :redis do
let(:status) { 'success' }
let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
+ let(:ref) { 'master' }
before do
- Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{project.id}/build_status", { sha: sha, status: status }) }
+ Gitlab::Redis.with do |redis|
+ redis.mapped_hmset(cache_key,
+ { sha: sha, status: status, ref: ref })
+ end
end
describe '#load_from_cache' do
@@ -168,6 +281,7 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
expect(pipeline_status.sha).to eq(sha)
expect(pipeline_status.status).to eq(status)
+ expect(pipeline_status.ref).to eq(ref)
end
end
@@ -181,7 +295,7 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus do
it 'deletes values from redis' do
pipeline_status.delete_from_cache
- key_exists = Gitlab::Redis.with { |redis| redis.exists("projects/#{project.id}/build_status") }
+ key_exists = Gitlab::Redis.with { |redis| redis.exists(cache_key) }
expect(key_exists).to be_falsy
end
diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
index 7a84bbebd02..bc66ce83d4a 100644
--- a/spec/lib/gitlab/checks/force_push_spec.rb
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -1,19 +1,19 @@
require 'spec_helper'
-describe Gitlab::Checks::ChangeAccess, lib: true do
+describe Gitlab::Checks::ForcePush, lib: true do
let(:project) { create(:project, :repository) }
context "exit code checking" do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
- expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
+ expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
end
it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do
allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
- expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError)
+ expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError)
end
end
end
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index 03f040f4465..40ac5a3ed37 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -71,12 +71,20 @@ describe Gitlab::Ci::Trace::Stream do
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)
@@ -84,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
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index 994995b57b8..a10a251dc4a 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
@@ -569,13 +569,8 @@ describe Gitlab::Diff::PositionTracer, lib: true do
# 1 1 BB
# 2 2 A
- it "returns the new position" do
- expect_new_position(
- old_path: file_name,
- new_path: new_file_name,
- old_line: old_position.new_line,
- new_line: old_position.new_line
- )
+ it "returns nil since the line doesn't exist in the new diffs anymore" do
+ expect(subject).to be_nil
end
end
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 2a86b427806..f127e45ae6a 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -7,9 +7,17 @@ describe Gitlab::Email::Receiver, lib: true do
context "when we cannot find a capable handler" do
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "!!!") }
- it "raises a UnknownIncomingEmail" do
+ it "raises an UnknownIncomingEmail error" do
expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
end
+
+ context "and the email contains no references header" do
+ let(:email_raw) { fixture_file("emails/auto_reply.eml").gsub(mail_key, "!!!") }
+
+ it "raises an UnknownIncomingEmail error" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
+ end
+ end
end
context "when the email is blank" do
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/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 3d6d7292b42..1b78910fa3c 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1031,6 +1031,35 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#find_commits' do
+ it 'should return a return a collection of commits' do
+ commits = repository.find_commits
+
+ expect(commits).not_to be_empty
+ expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) )
+ end
+
+ context 'while applying a sort order based on the `order` option' do
+ it "allows ordering topologically (no parents shown before their children)" do
+ expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO)
+
+ repository.find_commits(order: :topo)
+ end
+
+ it "allows ordering by date" do
+ expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE)
+
+ repository.find_commits(order: :date)
+ end
+
+ it "applies no sorting by default" do
+ expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE)
+
+ repository.find_commits
+ end
+ end
+ end
+
describe '#branches with deleted branch' do
before(:each) do
ref = double()
@@ -1045,20 +1074,8 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#branch_count' do
- before(:each) do
- valid_ref = double(:ref)
- invalid_ref = double(:ref)
-
- allow(valid_ref).to receive_messages(name: 'master', target: double(:target))
-
- allow(invalid_ref).to receive_messages(name: 'bad-branch')
- allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError }
-
- allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref])
- end
-
it 'returns the number of branches' do
- expect(repository.branch_count).to eq(1)
+ expect(repository.branch_count).to eq(9)
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/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb
index cc8daa535d6..cc8daa535d6 100644
--- a/spec/lib/git_ref_validator_spec.rb
+++ b/spec/lib/gitlab/git_ref_validator_spec.rb
diff --git a/spec/lib/gitlab/healthchecks/db_check_spec.rb b/spec/lib/gitlab/health_checks/db_check_spec.rb
index 33c6c24449c..33c6c24449c 100644
--- a/spec/lib/gitlab/healthchecks/db_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/db_check_spec.rb
diff --git a/spec/lib/gitlab/healthchecks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
index 4cd8cf313a5..4cd8cf313a5 100644
--- a/spec/lib/gitlab/healthchecks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
diff --git a/spec/lib/gitlab/healthchecks/redis_check_spec.rb b/spec/lib/gitlab/health_checks/redis_check_spec.rb
index 734cdcb893e..734cdcb893e 100644
--- a/spec/lib/gitlab/healthchecks/redis_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/redis_check_spec.rb
diff --git a/spec/lib/gitlab/healthchecks/simple_check_shared.rb b/spec/lib/gitlab/health_checks/simple_check_shared.rb
index 1fa6d0faef9..1fa6d0faef9 100644
--- a/spec/lib/gitlab/healthchecks/simple_check_shared.rb
+++ b/spec/lib/gitlab/health_checks/simple_check_shared.rb
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 7a0b0b06d4b..bfecfa28ed1 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6981,28 +6981,6 @@
],
"services": [
{
- "id": 164,
- "title": null,
- "project_id": 5,
- "created_at": "2016-06-14T15:02:07.372Z",
- "updated_at": "2016-06-14T15:02:07.372Z",
- "active": false,
- "properties": {
-
- },
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "build_events": true,
- "category": "issue_tracker",
- "type": "CustomIssueTrackerService",
- "default": true,
- "wiki_page_events": true
- },
- {
"id": 100,
"title": "JetBrains TeamCity CI",
"project_id": 5,
@@ -7019,6 +6997,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "TeamcityService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7040,6 +7019,7 @@
"tag_push_events": true,
"note_events": true,
"pipeline_events": true,
+ "type": "SlackService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7061,6 +7041,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "RedmineService",
"category": "issue_tracker",
"default": false,
"wiki_page_events": true
@@ -7082,6 +7063,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "PushoverService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7103,6 +7085,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "PivotalTrackerService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7125,6 +7108,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "JiraService",
"category": "issue_tracker",
"default": false,
"wiki_page_events": true
@@ -7146,6 +7130,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "IrkerService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7167,6 +7152,7 @@
"tag_push_events": true,
"note_events": true,
"pipeline_events": true,
+ "type": "HipchatService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7188,6 +7174,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "GemnasiumService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7209,6 +7196,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "FlowdockService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7230,6 +7218,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "ExternalWikiService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7251,6 +7240,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "EmailsOnPushService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7272,6 +7262,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "DroneCiService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7293,6 +7284,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "CustomIssueTrackerService",
"category": "issue_tracker",
"default": false,
"wiki_page_events": true
@@ -7314,6 +7306,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "CampfireService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7335,6 +7328,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "BuildkiteService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7356,6 +7350,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "BambooService",
"category": "ci",
"default": false,
"wiki_page_events": true
@@ -7377,6 +7372,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "AssemblaService",
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7398,6 +7394,7 @@
"tag_push_events": true,
"note_events": true,
"build_events": true,
+ "type": "AssemblaService",
"category": "common",
"default": false,
"wiki_page_events": true
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index fcc23a75ca1..06cd8ab87ed 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -60,7 +60,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
end
context 'original service exists' do
- let(:service_id) { Service.create(project: project).id }
+ let(:service_id) { create(:service, project: project).id }
it 'does not have the original service_id' do
expect(created_object.service_id).not_to eq(service_id)
diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
index 071e5fac3f0..071e5fac3f0 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
diff --git a/spec/lib/gitlab/issuable_sorter_spec.rb b/spec/lib/gitlab/issuable_sorter_spec.rb
new file mode 100644
index 00000000000..c9a434b2bcf
--- /dev/null
+++ b/spec/lib/gitlab/issuable_sorter_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::IssuableSorter, lib: true do
+ let(:namespace1) { build(:namespace, id: 1) }
+ let(:project1) { build(:project, id: 1, namespace: namespace1) }
+
+ let(:project2) { build(:project, id: 2, path: "a", namespace: project1.namespace) }
+ let(:project3) { build(:project, id: 3, path: "b", namespace: project1.namespace) }
+
+ let(:namespace2) { build(:namespace, id: 2, path: "a") }
+ let(:namespace3) { build(:namespace, id: 3, path: "b") }
+ let(:project4) { build(:project, id: 4, path: "a", namespace: namespace2) }
+ let(:project5) { build(:project, id: 5, path: "b", namespace: namespace2) }
+ let(:project6) { build(:project, id: 6, path: "a", namespace: namespace3) }
+
+ let(:unsorted) { [sorted[2], sorted[3], sorted[0], sorted[1]] }
+
+ let(:sorted) do
+ [build(:issue, iid: 1, project: project1),
+ build(:issue, iid: 2, project: project1),
+ build(:issue, iid: 10, project: project1),
+ build(:issue, iid: 20, project: project1)]
+ end
+
+ it 'sorts references by a given key' do
+ expect(described_class.sort(project1, unsorted)).to eq(sorted)
+ end
+
+ context 'for JIRA issues' do
+ let(:sorted) do
+ [ExternalIssue.new('JIRA-1', project1),
+ ExternalIssue.new('JIRA-2', project1),
+ ExternalIssue.new('JIRA-10', project1),
+ ExternalIssue.new('JIRA-20', project1)]
+ end
+
+ it 'sorts references by a given key' do
+ expect(described_class.sort(project1, unsorted)).to eq(sorted)
+ end
+ end
+
+ context 'for references from multiple projects and namespaces' do
+ let(:sorted) do
+ [build(:issue, iid: 1, project: project1),
+ build(:issue, iid: 2, project: project1),
+ build(:issue, iid: 10, project: project1),
+ build(:issue, iid: 1, project: project2),
+ build(:issue, iid: 1, project: project3),
+ build(:issue, iid: 1, project: project4),
+ build(:issue, iid: 1, project: project5),
+ build(:issue, iid: 1, project: project6)]
+ end
+ let(:unsorted) do
+ [sorted[3], sorted[1], sorted[4], sorted[2],
+ sorted[6], sorted[5], sorted[0], sorted[7]]
+ end
+
+ it 'sorts references by project and then by a given key' do
+ expect(subject.sort(project1, unsorted)).to eq(sorted)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 346cf0d117c..f4aab429931 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -108,6 +108,31 @@ describe Gitlab::LDAP::User, lib: true do
it "creates a new user if not found" do
expect{ ldap_user.save }.to change{ User.count }.by(1)
end
+
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting signup_enabled: false
+ end
+
+ it 'creates the user' do
+ ldap_user.save
+
+ expect(gl_user).to be_persisted
+ end
+ end
+
+ context 'when user confirmation email is enabled' do
+ before do
+ stub_application_setting send_user_confirmation_email: true
+ end
+
+ it 'creates and confirms the user anyway' do
+ ldap_user.save
+
+ expect(gl_user).to be_persisted
+ expect(gl_user).to be_confirmed
+ end
+ end
end
describe 'updating email' do
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 8f09266c3b3..828c953197d 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -40,6 +40,35 @@ describe Gitlab::OAuth::User, lib: true do
let(:provider) { 'twitter' }
describe 'signup' do
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting signup_enabled: false
+ end
+
+ it 'creates the user' do
+ stub_omniauth_config(allow_single_sign_on: ['twitter'])
+
+ oauth_user.save
+
+ expect(gl_user).to be_persisted
+ end
+ end
+
+ context 'when user confirmation email is enabled' do
+ before do
+ stub_application_setting send_user_confirmation_email: true
+ end
+
+ it 'creates and confirms the user anyway' do
+ stub_omniauth_config(allow_single_sign_on: ['twitter'])
+
+ oauth_user.save
+
+ expect(gl_user).to be_persisted
+ expect(gl_user).to be_confirmed
+ end
+ end
+
it 'marks user as having password_automatically_set' do
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
diff --git a/spec/lib/gitlab/other_markup.rb b/spec/lib/gitlab/other_markup_spec.rb
index 8f5a353b381..d6d53e8586c 100644
--- a/spec/lib/gitlab/other_markup.rb
+++ b/spec/lib/gitlab/other_markup_spec.rb
@@ -1,17 +1,19 @@
require 'spec_helper'
describe Gitlab::OtherMarkup, lib: true do
+ let(:context) { {} }
+
context "XSS Checks" do
links = {
'links' => {
file: 'file.rdoc',
input: 'XSS[JaVaScriPt:alert(1)]',
- output: '<p><a>XSS</a></p>'
+ output: "\n" + '<p><a>XSS</a></p>' + "\n"
}
}
links.each do |name, data|
it "does not convert dangerous #{name} into HTML" do
- expect(render(data[:file], data[:input], context)).to eql data[:output]
+ expect(render(data[:file], data[:input])).to eq(data[:output])
end
end
end
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/request_profiler_spec.rb b/spec/lib/gitlab/request_profiler_spec.rb
new file mode 100644
index 00000000000..ae9c06ebb7d
--- /dev/null
+++ b/spec/lib/gitlab/request_profiler_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::RequestProfiler, lib: true do
+ describe '.profile_token' do
+ it 'returns a token' do
+ expect(described_class.profile_token).to be_present
+ end
+
+ it 'caches the token' do
+ expect(Rails.cache).to receive(:fetch).with('profile-token')
+
+ described_class.profile_token
+ end
+ end
+
+ describe '.remove_all_profiles' do
+ it 'removes Gitlab::RequestProfiler::PROFILES_DIR directory' do
+ dir = described_class::PROFILES_DIR
+ FileUtils.mkdir_p(dir)
+
+ expect(Dir.exist?(dir)).to be true
+
+ described_class.remove_all_profiles
+ expect(Dir.exist?(dir)).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 4f6ef3c10fc..b106d156b75 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -211,6 +211,31 @@ describe Gitlab::Saml::User, lib: true do
end
end
end
+
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting signup_enabled: false
+ end
+
+ it 'creates the user' do
+ saml_user.save
+
+ expect(gl_user).to be_persisted
+ end
+ end
+
+ context 'when user confirmation email is enabled' do
+ before do
+ stub_application_setting send_user_confirmation_email: true
+ end
+
+ it 'creates and confirms the user anyway' do
+ saml_user.save
+
+ expect(gl_user).to be_persisted
+ expect(gl_user).to be_confirmed
+ end
+ end
end
describe 'blocking' do
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 6675d26734e..6675d26734e 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
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_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index 611cdbbc865..2b27ff66c09 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -87,10 +87,10 @@ describe Gitlab::UserAccess, lib: true do
expect(access.can_push_to_branch?(branch.name)).to be_falsey
end
- it 'returns true if branch does not exist and user has permission to merge' do
+ it 'returns false if branch does not exist' do
project.team << [user, :developer]
- expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy
+ expect(access.can_push_to_branch?(not_existing_branch.name)).to be_falsey
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/lib/light_url_builder_spec.rb b/spec/lib/light_url_builder_spec.rb
deleted file mode 100644
index 3fe8cf43934..00000000000
--- a/spec/lib/light_url_builder_spec.rb
+++ /dev/null
@@ -1,119 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::UrlBuilder, lib: true do
- describe '.build' do
- context 'when passing a Commit' do
- it 'returns a proper URL' do
- commit = build_stubbed(:commit)
-
- url = described_class.build(commit)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}"
- end
- end
-
- context 'when passing an Issue' do
- it 'returns a proper URL' do
- issue = build_stubbed(:issue, iid: 42)
-
- url = described_class.build(issue)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}"
- end
- end
-
- context 'when passing a MergeRequest' do
- it 'returns a proper URL' do
- merge_request = build_stubbed(:merge_request, iid: 42)
-
- url = described_class.build(merge_request)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}"
- end
- end
-
- context 'when passing a Note' do
- context 'on a Commit' do
- it 'returns a proper URL' do
- note = build_stubbed(:note_on_commit)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
- end
- end
-
- context 'on a Commit Diff' do
- it 'returns a proper URL' do
- note = build_stubbed(:diff_note_on_commit)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
- end
- end
-
- context 'on an Issue' do
- it 'returns a proper URL' do
- issue = create(:issue, iid: 42)
- note = build_stubbed(:note_on_issue, noteable: issue)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}"
- end
- end
-
- context 'on a MergeRequest' do
- it 'returns a proper URL' do
- merge_request = create(:merge_request, iid: 42)
- note = build_stubbed(:note_on_merge_request, noteable: merge_request)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
- end
- end
-
- context 'on a MergeRequest Diff' do
- it 'returns a proper URL' do
- merge_request = create(:merge_request, iid: 42)
- note = build_stubbed(:diff_note_on_merge_request, noteable: merge_request)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
- end
- end
-
- context 'on a ProjectSnippet' do
- it 'returns a proper URL' do
- project_snippet = create(:project_snippet)
- note = build_stubbed(:note_on_project_snippet, noteable: project_snippet)
-
- url = described_class.build(note)
-
- expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}"
- end
- end
-
- context 'on another object' do
- it 'returns a proper URL' do
- project = build_stubbed(:empty_project)
-
- expect { described_class.build(project) }.
- to raise_error(NotImplementedError, 'No URL builder defined for Project')
- end
- end
- end
-
- context 'when passing a WikiPage' do
- it 'returns a proper URL' do
- wiki_page = build(:wiki_page)
- url = described_class.build(wiki_page)
-
- expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}"
- end
- end
- end
-end
diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb
index e22858d1d8f..2ad572bb5c7 100644
--- a/spec/mailers/emails/merge_requests_spec.rb
+++ b/spec/mailers/emails/merge_requests_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'email_spec'
-describe Notify, "merge request notifications" do
+describe Emails::MergeRequests do
include EmailSpec::Matchers
describe "#resolved_all_discussions_email" do
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 5ca936f28f0..8c1c9bf135f 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'email_spec'
-describe Notify do
+describe Emails::Profile do
include EmailSpec::Matchers
include_context 'gitlab email notification'
@@ -15,106 +15,104 @@ describe Notify do
end
end
- describe 'profile notifications' do
- describe 'for new users, the email' do
- let(:example_site_path) { root_path }
- let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
- let(:token) { 'kETLwRaayvigPq_x3SNM' }
+ describe 'for new users, the email' do
+ let(:example_site_path) { root_path }
+ let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
+ let(:token) { 'kETLwRaayvigPq_x3SNM' }
- subject { Notify.new_user_email(new_user.id, token) }
+ subject { Notify.new_user_email(new_user.id, token) }
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'a new user email'
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'a new user email'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'contains the password text' do
- is_expected.to have_body_text /Click here to set your password/
- end
+ it 'contains the password text' do
+ is_expected.to have_body_text /Click here to set your password/
+ end
- it 'includes a link for user to set password' do
- params = "reset_password_token=#{token}"
- is_expected.to have_body_text(
- %r{http://#{Gitlab.config.gitlab.host}(:\d+)?/users/password/edit\?#{params}}
- )
- end
+ it 'includes a link for user to set password' do
+ params = "reset_password_token=#{token}"
+ is_expected.to have_body_text(
+ %r{http://#{Gitlab.config.gitlab.host}(:\d+)?/users/password/edit\?#{params}}
+ )
+ end
- it 'explains the reset link expiration' do
- is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/)
- is_expected.to have_body_text(new_user_password_url)
- is_expected.to have_body_text(/\?user_email=.*%40.*/)
- end
+ it 'explains the reset link expiration' do
+ is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/)
+ is_expected.to have_body_text(new_user_password_url)
+ is_expected.to have_body_text(/\?user_email=.*%40.*/)
end
+ end
- describe 'for users that signed up, the email' do
- let(:example_site_path) { root_path }
- let(:new_user) { create(:user, email: new_user_address, password: "securePassword") }
+ describe 'for users that signed up, the email' do
+ let(:example_site_path) { root_path }
+ let(:new_user) { create(:user, email: new_user_address, password: "securePassword") }
- subject { Notify.new_user_email(new_user.id) }
+ subject { Notify.new_user_email(new_user.id) }
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'a new user email'
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'a new user email'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'does not contain the new user\'s password' do
- is_expected.not_to have_body_text /password/
- end
+ it 'does not contain the new user\'s password' do
+ is_expected.not_to have_body_text /password/
end
+ end
- describe 'user added ssh key' do
- let(:key) { create(:personal_key) }
+ describe 'user added ssh key' do
+ let(:key) { create(:personal_key) }
- subject { Notify.new_ssh_key_email(key.id) }
+ subject { Notify.new_ssh_key_email(key.id) }
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'is sent to the new user' do
- is_expected.to deliver_to key.user.email
- end
+ it 'is sent to the new user' do
+ is_expected.to deliver_to key.user.email
+ end
- it 'has the correct subject' do
- is_expected.to have_subject /^SSH key was added to your account$/i
- end
+ it 'has the correct subject' do
+ is_expected.to have_subject /^SSH key was added to your account$/i
+ end
- it 'contains the new ssh key title' do
- is_expected.to have_body_text /#{key.title}/
- end
+ it 'contains the new ssh key title' do
+ is_expected.to have_body_text /#{key.title}/
+ end
- it 'includes a link to ssh keys page' do
- is_expected.to have_body_text /#{profile_keys_path}/
- end
+ it 'includes a link to ssh keys page' do
+ is_expected.to have_body_text /#{profile_keys_path}/
+ end
- context 'with SSH key that does not exist' do
- it { expect { Notify.new_ssh_key_email('foo') }.not_to raise_error }
- end
+ context 'with SSH key that does not exist' do
+ it { expect { Notify.new_ssh_key_email('foo') }.not_to raise_error }
end
+ end
- describe 'user added email' do
- let(:email) { create(:email) }
+ describe 'user added email' do
+ let(:email) { create(:email) }
- subject { Notify.new_email_email(email.id) }
+ subject { Notify.new_email_email(email.id) }
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'is sent to the new user' do
- is_expected.to deliver_to email.user.email
- end
+ it 'is sent to the new user' do
+ is_expected.to deliver_to email.user.email
+ end
- it 'has the correct subject' do
- is_expected.to have_subject /^Email was added to your account$/i
- end
+ it 'has the correct subject' do
+ is_expected.to have_subject /^Email was added to your account$/i
+ end
- it 'contains the new email address' do
- is_expected.to have_body_text /#{email.email}/
- end
+ it 'contains the new email address' do
+ is_expected.to have_body_text /#{email.email}/
+ end
- it 'includes a link to emails page' do
- is_expected.to have_body_text /#{profile_emails_path}/
- end
+ it 'includes a link to emails page' do
+ is_expected.to have_body_text /#{profile_emails_path}/
end
end
end
diff --git a/spec/migrations/schema_spec.rb b/spec/migrations/active_record/schema_spec.rb
index e132529d8d8..e132529d8d8 100644
--- a/spec/migrations/schema_spec.rb
+++ b/spec/migrations/active_record/schema_spec.rb
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/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 01ca1584ed2..c2c19c62048 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -4,6 +4,7 @@ describe ApplicationSetting, models: true do
let(:setting) { ApplicationSetting.create_from_defaults }
it { expect(setting).to be_valid }
+ it { expect(setting.uuid).to be_present }
describe 'validations' do
let(:http) { 'http://example.com' }
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index e5dd57fc4bb..7e8a1c8add7 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -2,6 +2,14 @@
require 'rails_helper'
describe Blob do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
describe '.decorate' do
it 'returns NilClass when given nil' do
expect(described_class.decorate(nil)).to be_nil
@@ -12,7 +20,7 @@ describe Blob do
context 'using a binary blob' do
it 'returns the data as-is' do
data = "\n\xFF\xB9\xC3"
- blob = described_class.new(double(binary?: true, data: data))
+ blob = fake_blob(binary: true, data: data)
expect(blob.data).to eq(data)
end
@@ -20,202 +28,176 @@ describe Blob do
context 'using a text blob' do
it 'converts the data to UTF-8' do
- blob = described_class.new(double(binary?: false, data: "\n\xFF\xB9\xC3"))
+ blob = fake_blob(binary: false, data: "\n\xFF\xB9\xC3")
expect(blob.data).to eq("\n���")
end
end
end
- describe '#svg?' do
- it 'is falsey when not text' do
- git_blob = double(text?: false)
+ describe '#raw_binary?' do
+ context 'if the blob is a valid LFS pointer' do
+ context 'if the extension has a rich viewer' do
+ context 'if the viewer is binary' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- expect(described_class.decorate(git_blob)).not_to be_svg
- end
-
- it 'is falsey when no language is detected' do
- git_blob = double(text?: true, language: nil)
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
- expect(described_class.decorate(git_blob)).not_to be_svg
- end
+ context 'if the viewer is text-based' do
+ it 'return false' do
+ blob = fake_blob(path: 'file.md', lfs: true)
- it' is falsey when language is not SVG' do
- git_blob = double(text?: true, language: double(name: 'XML'))
-
- expect(described_class.decorate(git_blob)).not_to be_svg
- end
-
- it 'is truthy when language is SVG' do
- git_blob = double(text?: true, language: double(name: 'SVG'))
-
- expect(described_class.decorate(git_blob)).to be_svg
- end
- end
-
- describe '#pdf?' do
- it 'is falsey when file extension is not .pdf' do
- git_blob = Gitlab::Git::Blob.new(name: 'git_blob.txt')
-
- expect(described_class.decorate(git_blob)).not_to be_pdf
- end
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+ end
- it 'is truthy when file extension is .pdf' do
- git_blob = Gitlab::Git::Blob.new(name: 'git_blob.pdf')
+ context "if the extension doesn't have a rich viewer" do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.exe', lfs: true)
- expect(described_class.decorate(git_blob)).to be_pdf
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
end
- end
- describe '#ipython_notebook?' do
- it 'is falsey when language is not Jupyter Notebook' do
- git_blob = double(text?: true, language: double(name: 'JSON'))
+ context 'if the blob is not an LFS pointer' do
+ context 'if the blob is binary' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.pdf', binary: true)
- expect(described_class.decorate(git_blob)).not_to be_ipython_notebook
- end
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
- it 'is truthy when language is Jupyter Notebook' do
- git_blob = double(text?: true, language: double(name: 'Jupyter Notebook'))
+ context 'if the blob is text-based' do
+ it 'return false' do
+ blob = fake_blob(path: 'file.md')
- expect(described_class.decorate(git_blob)).to be_ipython_notebook
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
end
end
- describe '#sketch?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: "design.png")
-
- expect(described_class.decorate(git_blob)).not_to be_sketch
- end
-
- it 'is truthy with sketch extension' do
- git_blob = Gitlab::Git::Blob.new(name: "design.sketch")
+ describe '#extension' do
+ it 'returns the extension' do
+ blob = fake_blob(path: 'file.md')
- expect(described_class.decorate(git_blob)).to be_sketch
+ expect(blob.extension).to eq('md')
end
end
- describe '#video?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'image.png')
+ describe '#simple_viewer' do
+ context 'when the blob is empty' do
+ it 'returns an empty viewer' do
+ blob = fake_blob(data: '')
- expect(described_class.decorate(git_blob)).not_to be_video
- end
-
- UploaderHelper::VIDEO_EXT.each do |ext|
- it "is truthy when extension is .#{ext}" do
- git_blob = Gitlab::Git::Blob.new(name: "video.#{ext}")
-
- expect(described_class.decorate(git_blob)).to be_video
+ expect(blob.simple_viewer).to be_a(BlobViewer::Empty)
end
end
- end
- describe '#stl?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'file.png')
+ context 'when the file represented by the blob is binary' do
+ it 'returns a download viewer' do
+ blob = fake_blob(binary: true)
- expect(described_class.decorate(git_blob)).not_to be_stl
+ expect(blob.simple_viewer).to be_a(BlobViewer::Download)
+ end
end
- it 'is truthy with STL extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'file.stl')
+ context 'when the file represented by the blob is text-based' do
+ it 'returns a text viewer' do
+ blob = fake_blob
- expect(described_class.decorate(git_blob)).to be_stl
+ expect(blob.simple_viewer).to be_a(BlobViewer::Text)
+ end
end
end
- describe '#to_partial_path' do
- let(:project) { double(lfs_enabled?: true) }
+ describe '#rich_viewer' do
+ context 'when the blob is an invalid LFS pointer' do
+ before do
+ project.lfs_enabled = false
+ end
- def stubbed_blob(overrides = {})
- overrides.reverse_merge!(
- name: nil,
- image?: false,
- language: nil,
- lfs_pointer?: false,
- svg?: false,
- text?: false,
- binary?: false,
- stl?: false
- )
+ it 'returns nil' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- described_class.decorate(Gitlab::Git::Blob.new({})).tap do |blob|
- allow(blob).to receive_messages(overrides)
+ expect(blob.rich_viewer).to be_nil
end
end
- it 'handles LFS pointers with LFS enabled' do
- blob = stubbed_blob(lfs_pointer?: true, text?: true)
- expect(blob.to_partial_path(project)).to eq 'download'
- end
-
- it 'handles LFS pointers with LFS disabled' do
- blob = stubbed_blob(lfs_pointer?: true, text?: true)
- project = double(lfs_enabled?: false)
- expect(blob.to_partial_path(project)).to eq 'text'
- end
+ context 'when the blob is empty' do
+ it 'returns nil' do
+ blob = fake_blob(data: '')
- it 'handles SVGs' do
- blob = stubbed_blob(text?: true, svg?: true)
- expect(blob.to_partial_path(project)).to eq 'svg'
+ expect(blob.rich_viewer).to be_nil
+ end
end
- it 'handles images' do
- blob = stubbed_blob(image?: true)
- expect(blob.to_partial_path(project)).to eq 'image'
- end
+ context 'when the blob is a valid LFS pointer' do
+ it 'returns a matching viewer' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- it 'handles text' do
- blob = stubbed_blob(text?: true, name: 'test.txt')
- expect(blob.to_partial_path(project)).to eq 'text'
- end
-
- it 'defaults to download' do
- blob = stubbed_blob
- expect(blob.to_partial_path(project)).to eq 'download'
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
+ end
end
- it 'handles PDFs' do
- blob = stubbed_blob(name: 'blob.pdf', pdf?: true)
- expect(blob.to_partial_path(project)).to eq 'pdf'
- end
+ context 'when the blob is binary' do
+ it 'returns a matching binary viewer' do
+ blob = fake_blob(path: 'file.pdf', binary: true)
- it 'handles iPython notebooks' do
- blob = stubbed_blob(text?: true, ipython_notebook?: true)
- expect(blob.to_partial_path(project)).to eq 'notebook'
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
+ end
end
- it 'handles Sketch files' do
- blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
- expect(blob.to_partial_path(project)).to eq 'sketch'
- end
+ context 'when the blob is text-based' do
+ it 'returns a matching text-based viewer' do
+ blob = fake_blob(path: 'file.md')
- it 'handles STLs' do
- blob = stubbed_blob(text?: true, stl?: true)
- expect(blob.to_partial_path(project)).to eq 'stl'
+ expect(blob.rich_viewer).to be_a(BlobViewer::Markup)
+ end
end
end
- describe '#size_within_svg_limits?' do
- let(:blob) { described_class.decorate(double(:blob)) }
+ describe '#rendered_as_text?' do
+ context 'when ignoring errors' do
+ context 'when the simple viewer is text-based' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.md', size: 100.megabytes)
- it 'returns true when the blob size is smaller than the SVG limit' do
- expect(blob).to receive(:size).and_return(42)
+ expect(blob.rendered_as_text?).to be_truthy
+ end
+ end
+
+ context 'when the simple viewer is binary' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes)
- expect(blob.size_within_svg_limits?).to eq(true)
+ expect(blob.rendered_as_text?).to be_falsey
+ end
+ end
end
- it 'returns true when the blob size is equal to the SVG limit' do
- expect(blob).to receive(:size).and_return(Blob::MAXIMUM_SVG_SIZE)
+ context 'when not ignoring errors' do
+ context 'when the viewer has render errors' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.md', size: 100.megabytes)
- expect(blob.size_within_svg_limits?).to eq(true)
- end
+ expect(blob.rendered_as_text?(ignore_errors: false)).to be_falsey
+ end
+ end
- it 'returns false when the blob size is larger than the SVG limit' do
- expect(blob).to receive(:size).and_return(1.terabyte)
+ context "when the viewer doesn't have render errors" do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.md')
- expect(blob.size_within_svg_limits?).to eq(false)
+ expect(blob.rendered_as_text?(ignore_errors: false)).to be_truthy
+ end
+ end
end
end
end
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
new file mode 100644
index 00000000000..a3e598de56d
--- /dev/null
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -0,0 +1,186 @@
+require 'spec_helper'
+
+describe BlobViewer::Base, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project) }
+
+ let(:viewer_class) do
+ Class.new(described_class) do
+ self.extensions = %w(pdf)
+ self.max_size = 1.megabyte
+ self.absolute_max_size = 5.megabytes
+ self.client_side = false
+ end
+ end
+
+ let(:viewer) { viewer_class.new(blob) }
+
+ describe '.can_render?' do
+ context 'when the extension is supported' do
+ let(:blob) { fake_blob(path: 'file.pdf') }
+
+ it 'returns true' do
+ expect(viewer_class.can_render?(blob)).to be_truthy
+ end
+ end
+
+ context 'when the extension is not supported' do
+ let(:blob) { fake_blob(path: 'file.txt') }
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(blob)).to be_falsey
+ end
+ end
+ end
+
+ describe '#too_large?' do
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.too_large?).to be_truthy
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns false' do
+ expect(viewer.too_large?).to be_falsey
+ end
+ end
+ end
+
+ describe '#absolutely_too_large?' do
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.absolutely_too_large?).to be_truthy
+ end
+ end
+
+ context 'when the blob size is smaller than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns false' do
+ expect(viewer.absolutely_too_large?).to be_falsey
+ end
+ end
+ end
+
+ describe '#can_override_max_size?' do
+ context 'when the blob size is larger than the max size' do
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns false' do
+ expect(viewer.can_override_max_size?).to be_falsey
+ end
+ end
+
+ context 'when the blob size is smaller than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.can_override_max_size?).to be_truthy
+ end
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns false' do
+ expect(viewer.can_override_max_size?).to be_falsey
+ end
+ end
+ end
+
+ describe '#render_error' do
+ context 'when the max size is overridden' do
+ before do
+ viewer.override_max_size = true
+ end
+
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns :too_large' do
+ expect(viewer.render_error).to eq(:too_large)
+ end
+ end
+
+ context 'when the blob size is smaller than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns nil' do
+ expect(viewer.render_error).to be_nil
+ end
+ end
+ end
+
+ context 'when the max size is not overridden' do
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns :too_large' do
+ expect(viewer.render_error).to eq(:too_large)
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns nil' do
+ expect(viewer.render_error).to be_nil
+ end
+ end
+ end
+
+ context 'when the viewer is server side but the blob is stored in LFS' do
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ it 'return :server_side_but_stored_in_lfs' do
+ expect(viewer.render_error).to eq(:server_side_but_stored_in_lfs)
+ end
+ end
+ end
+
+ describe '#prepare!' do
+ context 'when the viewer is server side' do
+ let(:blob) { fake_blob(path: 'file.md') }
+
+ before do
+ viewer_class.client_side = false
+ end
+
+ it 'loads all blob data' do
+ expect(blob).to receive(:load_all_data!)
+
+ viewer.prepare!
+ end
+ end
+
+ context 'when the viewer is client side' do
+ let(:blob) { fake_blob(path: 'file.md') }
+
+ before do
+ viewer_class.client_side = true
+ end
+
+ it "doesn't load all blob data" do
+ expect(blob).not_to receive(:load_all_data!)
+
+ viewer.prepare!
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index d7d6a75d38d..3b222ea1c3d 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -296,32 +296,56 @@ describe Ci::Pipeline, models: true do
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
- let(:build) { create_build('build1', 0) }
- let(:build_b) { create_build('build2', 0) }
- let(:build_c) { create_build('build3', 0) }
+ let(:build) { create_build('build1', queued_at: 0) }
+ let(:build_b) { create_build('build2', queued_at: 0) }
+ let(:build_c) { create_build('build3', queued_at: 0) }
describe '#duration' do
- before do
- travel_to(current + 30) do
- build.run!
- build.success!
- build_b.run!
- build_c.run!
- end
+ context 'when multiple builds are finished' do
+ before do
+ travel_to(current + 30) do
+ build.run!
+ build.success!
+ build_b.run!
+ build_c.run!
+ end
- travel_to(current + 40) do
- build_b.drop!
+ travel_to(current + 40) do
+ build_b.drop!
+ end
+
+ travel_to(current + 70) do
+ build_c.success!
+ end
end
- travel_to(current + 70) do
- build_c.success!
+ it 'matches sum of builds duration' do
+ pipeline.reload
+
+ expect(pipeline.duration).to eq(40)
end
end
- it 'matches sum of builds duration' do
- pipeline.reload
+ context 'when pipeline becomes blocked' do
+ let!(:build) { create_build('build:1') }
+ let!(:action) { create_build('manual:action', :manual) }
+
+ before do
+ travel_to(current + 1.minute) do
+ build.run!
+ end
+
+ travel_to(current + 5.minutes) do
+ build.success!
+ end
+ end
+
+ it 'recalculates pipeline duration' do
+ pipeline.reload
- expect(pipeline.duration).to eq(40)
+ expect(pipeline).to be_manual
+ expect(pipeline.duration).to eq 4.minutes
+ end
end
end
@@ -376,19 +400,20 @@ describe Ci::Pipeline, models: true do
end
describe 'pipeline caching' do
- it 'executes ExpirePipelinesCacheService' do
- expect_any_instance_of(Ci::ExpirePipelineCacheService).to receive(:execute).with(pipeline)
+ it 'performs ExpirePipelinesCacheWorker' do
+ expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id)
pipeline.cancel
end
end
- def create_build(name, queued_at = current, started_from = 0)
- create(:ci_build,
+ def create_build(name, *traits, queued_at: current, started_from: 0, **opts)
+ create(:ci_build, *traits,
name: name,
pipeline: pipeline,
queued_at: queued_at,
- started_at: queued_at + started_from)
+ started_at: queued_at + started_from,
+ **opts)
end
end
@@ -1014,11 +1039,12 @@ describe Ci::Pipeline, models: true do
end
describe "#merge_requests" do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: 'a288a022a53a5a944fae87bcec6efc87b7061808') }
it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do
merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+ allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { 'a288a022a53a5a944fae87bcec6efc87b7061808' }
expect(pipeline.merge_requests).to eq([merge_request])
end
@@ -1037,6 +1063,23 @@ describe Ci::Pipeline, models: true do
end
end
+ describe "#all_merge_requests" do
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master') }
+
+ it "returns all merge requests having the same source branch" do
+ merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+
+ expect(pipeline.all_merge_requests).to eq([merge_request])
+ end
+
+ it "doesn't return merge requests having a different source branch" do
+ create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master')
+
+ expect(pipeline.all_merge_requests).to be_empty
+ end
+ end
+
describe '#stuck?' do
before do
create(:ci_build, :pending, pipeline: pipeline)
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index de791abdf3d..63ad3a3630b 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -1,10 +1,12 @@
require 'spec_helper'
-describe Issue, "Awardable" do
+describe Awardable do
let!(:issue) { create(:issue) }
let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) }
describe "Associations" do
+ subject { build(:issue) }
+
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 6151d53cd91..4edafbc4e32 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
@@ -21,24 +18,25 @@ describe CacheMarkdownField do
end
extend ActiveModel::Callbacks
- define_model_callbacks :save
+ define_model_callbacks :create, :update
include CacheMarkdownField
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,134 +46,252 @@ 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
+ run_callbacks :update do
changes_applied
end
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/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb
index 0002a00770f..8571e85627c 100644
--- a/spec/models/concerns/discussion_on_diff_spec.rb
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe DiffDiscussion, DiscussionOnDiff, model: true do
+describe DiscussionOnDiff, model: true do
subject { create(:diff_note_on_merge_request).to_discussion }
describe "#truncated_diff_lines" do
@@ -8,9 +8,9 @@ describe DiffDiscussion, DiscussionOnDiff, model: true do
context "when diff is greater than allowed number of truncated diff lines " do
it "returns fewer lines" do
- expect(subject.diff_lines.count).to be > described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
+ expect(subject.diff_lines.count).to be > DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
- expect(truncated_lines.count).to be <= described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
+ expect(truncated_lines.count).to be <= DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4522206fab1..3ecba2e9687 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -1,10 +1,13 @@
require 'spec_helper'
-describe Issue, "Issuable" do
+describe Issuable do
+ let(:issuable_class) { Issue }
let(:issue) { create(:issue) }
let(:user) { create(:user) }
describe "Associations" do
+ subject { build(:issue) }
+
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:author) }
it { is_expected.to belong_to(:assignee) }
@@ -23,10 +26,14 @@ describe Issue, "Issuable" do
end
describe 'Included modules' do
+ let(:described_class) { issuable_class }
+
it { is_expected.to include_module(Awardable) }
end
describe "Validation" do
+ subject { build(:issue) }
+
before do
allow(subject).to receive(:set_iid).and_return(false)
end
@@ -39,9 +46,11 @@ describe Issue, "Issuable" do
end
describe "Scope" do
- it { expect(described_class).to respond_to(:opened) }
- it { expect(described_class).to respond_to(:closed) }
- it { expect(described_class).to respond_to(:assigned) }
+ subject { build(:issue) }
+
+ it { expect(issuable_class).to respond_to(:opened) }
+ it { expect(issuable_class).to respond_to(:closed) }
+ it { expect(issuable_class).to respond_to(:assigned) }
end
describe 'author_name' do
@@ -115,16 +124,16 @@ describe Issue, "Issuable" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
it 'returns notes with a matching title' do
- expect(described_class.search(searchable_issue.title)).
+ expect(issuable_class.search(searchable_issue.title)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching title' do
- expect(described_class.search('able')).to eq([searchable_issue])
+ expect(issuable_class.search('able')).to eq([searchable_issue])
end
it 'returns notes with a matching title regardless of the casing' do
- expect(described_class.search(searchable_issue.title.upcase)).
+ expect(issuable_class.search(searchable_issue.title.upcase)).
to eq([searchable_issue])
end
end
@@ -135,31 +144,31 @@ describe Issue, "Issuable" do
end
it 'returns notes with a matching title' do
- expect(described_class.full_search(searchable_issue.title)).
+ expect(issuable_class.full_search(searchable_issue.title)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching title' do
- expect(described_class.full_search('able')).to eq([searchable_issue])
+ expect(issuable_class.full_search('able')).to eq([searchable_issue])
end
it 'returns notes with a matching title regardless of the casing' do
- expect(described_class.full_search(searchable_issue.title.upcase)).
+ expect(issuable_class.full_search(searchable_issue.title.upcase)).
to eq([searchable_issue])
end
it 'returns notes with a matching description' do
- expect(described_class.full_search(searchable_issue.description)).
+ expect(issuable_class.full_search(searchable_issue.description)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching description' do
- expect(described_class.full_search(searchable_issue.description)).
+ expect(issuable_class.full_search(searchable_issue.description)).
to eq([searchable_issue])
end
it 'returns notes with a matching description regardless of the casing' do
- expect(described_class.full_search(searchable_issue.description.upcase)).
+ expect(issuable_class.full_search(searchable_issue.description.upcase)).
to eq([searchable_issue])
end
end
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index 92cc8859a8c..bdae742ff1d 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe MergeRequest, Noteable, model: true do
+describe Noteable, model: true do
let!(:active_diff_note1) { create(:diff_note_on_merge_request) }
let(:project) { active_diff_note1.project }
subject { active_diff_note1.noteable }
diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb
index 255b584a85e..494e6f1b6f6 100644
--- a/spec/models/concerns/relative_positioning_spec.rb
+++ b/spec/models/concerns/relative_positioning_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Issue, 'RelativePositioning' do
+describe RelativePositioning do
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let(:issue1) { create(:issue, project: project) }
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index f191605dbdb..221647d7a48 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -194,6 +194,24 @@ describe Group, 'Routable' do
it { expect(group.full_path).to eq(group.path) }
it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") }
+
+ context 'with RequestStore active' do
+ before do
+ RequestStore.begin!
+ end
+
+ after do
+ RequestStore.end!
+ RequestStore.clear!
+ end
+
+ it 'does not load the route table more than once' do
+ expect(group).to receive(:uncached_full_path).once.and_call_original
+
+ 3.times { group.full_path }
+ expect(group.full_path).to eq(group.path)
+ end
+ end
end
describe '#full_name' do
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index fd3b8307571..e698207166c 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -1,9 +1,11 @@
require 'spec_helper'
-describe Issue, 'Spammable' do
+describe Spammable do
let(:issue) { create(:issue, description: 'Test Desc.') }
describe 'Associations' do
+ subject { build(:issue) }
+
it { is_expected.to have_one(:user_agent_detail).dependent(:destroy) }
end
diff --git a/spec/models/concerns/strip_attribute_spec.rb b/spec/models/concerns/strip_attribute_spec.rb
index c3af7a0960f..8c945686b66 100644
--- a/spec/models/concerns/strip_attribute_spec.rb
+++ b/spec/models/concerns/strip_attribute_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Milestone, "StripAttribute" do
+describe StripAttribute do
let(:milestone) { create(:milestone) }
describe ".strip_attributes" do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index d057c9cf6e9..11befd4edfe 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -361,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/member_spec.rb b/spec/models/member_spec.rb
index c720cc9f2c2..ccc3deac199 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -386,6 +386,33 @@ 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!(:admin) { create(:admin) }
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+
+ it 'returns a <Source>Member objects' do
+ members = described_class.add_users(source, [user1, user2], :master)
+
+ expect(members).to be_a Array
+ expect(members.size).to eq(2)
+ 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..be08b96641a 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -199,10 +199,10 @@ describe MergeRequest, models: true do
end
context 'when there are no MR diffs' do
- it 'delegates to the compare object' do
+ it 'delegates to the compare object, setting no_collapse: true' do
merge_request.compare = double(:compare)
- expect(merge_request.compare).to receive(:diffs).with(options)
+ expect(merge_request.compare).to receive(:diffs).with(options.merge(no_collapse: true))
merge_request.diffs(options)
end
@@ -215,15 +215,22 @@ describe MergeRequest, models: true do
end
context 'when there are MR diffs' do
- before do
+ it 'returns the correct count' do
merge_request.save
+
+ expect(merge_request.diff_size).to eq('105')
end
- it 'returns the correct count' do
- expect(merge_request.diff_size).to eq(105)
+ it 'returns the correct overflow count' do
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
+ merge_request.save
+
+ expect(merge_request.diff_size).to eq('2+')
end
it 'does not perform highlighting' do
+ merge_request.save
+
expect(Gitlab::Diff::Highlight).not_to receive(:new)
merge_request.diff_size
@@ -231,7 +238,7 @@ describe MergeRequest, models: true do
end
context 'when there are no MR diffs' do
- before do
+ def set_compare(merge_request)
merge_request.compare = CompareService.new(
merge_request.source_project,
merge_request.source_branch
@@ -242,10 +249,21 @@ describe MergeRequest, models: true do
end
it 'returns the correct count' do
- expect(merge_request.diff_size).to eq(105)
+ set_compare(merge_request)
+
+ expect(merge_request.diff_size).to eq('105')
+ end
+
+ it 'returns the correct overflow count' do
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
+ set_compare(merge_request)
+
+ expect(merge_request.diff_size).to eq('2+')
end
it 'does not perform highlighting' do
+ set_compare(merge_request)
+
expect(Gitlab::Diff::Highlight).not_to receive(:new)
merge_request.diff_size
@@ -820,15 +838,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/network/graph_spec.rb b/spec/models/network/graph_spec.rb
index 492c4e01bd8..46b36e11c23 100644
--- a/spec/models/network/graph_spec.rb
+++ b/spec/models/network/graph_spec.rb
@@ -9,4 +9,25 @@ describe Network::Graph, models: true do
expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
end
+
+ describe "#commits" do
+ let(:graph) { described_class.new(project, 'refs/heads/master', project.repository.commit, nil) }
+
+ it "returns a list of commits" do
+ commits = graph.commits
+
+ expect(commits).not_to be_empty
+ expect(commits).to all( be_kind_of(Network::Commit) )
+ end
+
+ it "sorts the commits by commit date (descending)" do
+ # Remove duplicate timestamps because they make it harder to
+ # assert that the commits are sorted as expected.
+ commits = graph.commits.uniq(&:date)
+ sorted_commits = commits.sort_by(&:date).reverse
+
+ expect(commits).not_to be_empty
+ expect(commits.map(&:id)).to eq(sorted_commits.map(&:id))
+ 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..8fbe42248ae 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 true' do
+ subject.project = create(:empty_project, :empty_repo)
+
+ expect(subject.can_test?).to be true
+ 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/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb
index fbe6f344a98..869b25b933b 100644
--- a/spec/models/project_services/issue_tracker_service_spec.rb
+++ b/spec/models/project_services/issue_tracker_service_spec.rb
@@ -8,7 +8,7 @@ describe IssueTrackerService, models: true do
let(:service) { RedmineService.new(project: project, active: true) }
before do
- create(:service, project: project, active: true, category: 'issue_tracker')
+ create(:custom_issue_tracker_service, project: project)
end
context 'when service is changed manually by user' do
diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb
index 03932895b0e..03932895b0e 100644
--- a/spec/models/project_services/pipeline_email_service_spec.rb
+++ b/spec/models/project_services/pipelines_email_service_spec.rb
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 92d420337f9..d9244657953 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -781,17 +781,14 @@ describe Project, models: true do
let(:project) { create(:empty_project) }
- context 'When avatar file is uploaded' do
- before do
- project.update_columns(avatar: 'uploads/avatar.png')
- allow(project.avatar).to receive(:present?) { true }
- end
+ context 'when avatar file is uploaded' do
+ let(:project) { create(:empty_project, :with_avatar) }
- let(:avatar_path) do
- "/uploads/project/avatar/#{project.id}/uploads/avatar.png"
- end
+ it 'creates a correct avatar path' do
+ avatar_path = "/uploads/project/avatar/#{project.id}/dk.png"
- it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
+ expect(project.avatar_url).to eq("http://#{Gitlab.config.gitlab.host}#{avatar_path}")
+ end
end
context 'When avatar file in git' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 74d5ebc6db0..5216764a82d 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1379,12 +1379,22 @@ describe Repository, models: true do
describe '#branch_count' do
it 'returns the number of branches' do
expect(repository.branch_count).to be_an(Integer)
+
+ # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
+ rugged_count = repository.raw_repository.rugged.branches.count
+
+ expect(repository.branch_count).to eq(rugged_count)
end
end
describe '#tag_count' do
it 'returns the number of tags' do
expect(repository.tag_count).to be_an(Integer)
+
+ # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
+ rugged_count = repository.raw_repository.rugged.tags.count
+
+ expect(repository.tag_count).to eq(rugged_count)
end
end
@@ -1803,9 +1813,9 @@ describe Repository, models: true do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches).
- with(%i(readme license_blob license_key))
+ with(%i(rendered_readme license_blob license_key))
- expect(repository).to receive(:readme)
+ expect(repository).to receive(:rendered_readme)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key)
@@ -1849,17 +1859,15 @@ describe Repository, models: true do
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
+ describe '#is_ancestor?' do
+ context 'Gitaly is_ancestor feature enabled' do
+ it "asks Gitaly server if it's an ancestor" do
+ commit = repository.commit
+ expect(repository.raw_repository).to receive(:is_ancestor?).and_call_original
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
+
+ expect(repository.is_ancestor?(commit.id, commit.id)).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
index 6b7eef388be..5710edbc9e0 100644
--- a/spec/models/sent_notification_spec.rb
+++ b/spec/models/sent_notification_spec.rb
@@ -69,6 +69,7 @@ describe SentNotification, model: true do
it 'creates a comment on the issue' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).not_to eq(note.discussion_id)
end
end
@@ -79,6 +80,7 @@ describe SentNotification, model: true do
it 'creates a reply on the discussion' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
end
end
@@ -99,6 +101,7 @@ describe SentNotification, model: true do
it 'creates a comment on the merge request' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).not_to eq(note.discussion_id)
end
end
@@ -109,6 +112,7 @@ describe SentNotification, model: true do
it 'creates a reply on the discussion' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
end
end
@@ -119,6 +123,7 @@ describe SentNotification, model: true do
it 'creates a reply on the discussion' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
end
end
@@ -140,6 +145,7 @@ describe SentNotification, model: true do
it 'creates a comment on the commit' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).not_to eq(note.discussion_id)
end
end
@@ -150,6 +156,7 @@ describe SentNotification, model: true do
it 'creates a reply on the discussion' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
end
end
@@ -160,6 +167,7 @@ describe SentNotification, model: true do
it 'creates a reply on the discussion' do
new_note = subject.create_reply('Test')
expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
end
end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 0e2f07e945f..134882648b9 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -6,44 +6,53 @@ describe Service, models: true do
it { is_expected.to have_one :service_hook }
end
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:type) }
+ end
+
describe "Test Button" do
- before do
- @service = Service.new
- end
+ describe '#can_test?' do
+ let(:service) { create(:service, project: project) }
- describe "Testable" do
- let(:project) { create(:project, :repository) }
+ context 'when repository is not empty' do
+ let(:project) { create(:project, :repository) }
- before do
- allow(@service).to receive(:project).and_return(project)
- @testable = @service.can_test?
+ it 'returns true' do
+ expect(service.can_test?).to be true
+ end
end
- describe '#can_test?' do
- it { expect(@testable).to eq(true) }
+ context 'when repository is empty' do
+ let(:project) { create(:empty_project) }
+
+ it 'returns true' do
+ expect(service.can_test?).to be true
+ end
end
+ end
+
+ describe '#test' do
+ let(:data) { 'test' }
+ let(:service) { create(:service, project: project) }
- describe '#test' do
- let(:data) { 'test' }
+ context 'when repository is not empty' do
+ let(:project) { create(:project, :repository) }
it 'test runs execute' do
- expect(@service).to receive(:execute).with(data)
+ expect(service).to receive(:execute).with(data)
- @service.test(data)
+ service.test(data)
end
end
- end
- describe "With commits" do
- let(:project) { create(:project, :repository) }
+ context 'when repository is empty' do
+ let(:project) { create(:empty_project) }
- before do
- allow(@service).to receive(:project).and_return(project)
- @testable = @service.can_test?
- end
+ it 'test runs execute' do
+ expect(service).to receive(:execute).with(data)
- describe '#can_test?' do
- it { expect(@testable).to eq(true) }
+ service.test(data)
+ end
end
end
end
diff --git a/spec/models/snippet_blob_spec.rb b/spec/models/snippet_blob_spec.rb
new file mode 100644
index 00000000000..120b390586b
--- /dev/null
+++ b/spec/models/snippet_blob_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe SnippetBlob, models: true do
+ let(:snippet) { create(:snippet) }
+
+ subject { described_class.new(snippet) }
+
+ describe '#id' do
+ it 'returns the snippet ID' do
+ expect(subject.id).to eq(snippet.id)
+ end
+ end
+
+ describe '#name' do
+ it 'returns the snippet file name' do
+ expect(subject.name).to eq(snippet.file_name)
+ end
+ end
+
+ describe '#size' do
+ it 'returns the data size' do
+ expect(subject.size).to eq(subject.data.bytesize)
+ end
+ end
+
+ describe '#data' do
+ it 'returns the snippet content' do
+ expect(subject.data).to eq(snippet.content)
+ end
+ end
+
+ describe '#rendered_markup' do
+ context 'when the content is GFM' do
+ let(:snippet) { create(:snippet, file_name: 'file.md') }
+
+ it 'returns the rendered GFM' do
+ expect(subject.rendered_markup).to eq(snippet.content_html)
+ end
+ end
+
+ context 'when the content is not GFM' do
+ it 'returns nil' do
+ expect(subject.rendered_markup).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 8095d01b69e..75b1fc7e216 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -5,7 +5,6 @@ describe Snippet, models: true do
subject { described_class }
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
- it { is_expected.to include_module(Linguist::BlobHelper) }
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
@@ -241,4 +240,16 @@ describe Snippet, models: true do
end
end
end
+
+ describe '#blob' do
+ let(:snippet) { create(:snippet) }
+
+ it 'returns a blob representing the snippet data' do
+ blob = snippet.blob
+
+ expect(blob).to be_a(Blob)
+ expect(blob.path).to eq(snippet.file_name)
+ expect(blob.data).to eq(snippet.content)
+ end
+ end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 581305ad39f..3f80e1ac534 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -125,4 +125,50 @@ describe Todo, models: true do
expect(subject.target_reference).to eq issue.to_reference(full: true)
end
end
+
+ describe '#self_added?' do
+ let(:user_1) { build(:user) }
+
+ before do
+ subject.user = user_1
+ end
+
+ it 'is true when the user is the author' do
+ subject.author = user_1
+
+ expect(subject).to be_self_added
+ end
+
+ it 'is false when the user is not the author' do
+ subject.author = build(:user)
+
+ expect(subject).not_to be_self_added
+ end
+ end
+
+ describe '#self_assigned?' do
+ let(:user_1) { build(:user) }
+
+ before do
+ subject.user = user_1
+ subject.author = user_1
+ subject.action = Todo::ASSIGNED
+ end
+
+ it 'is true when todo is ASSIGNED and self_added' do
+ expect(subject).to be_self_assigned
+ end
+
+ it 'is false when the todo is not ASSIGNED' do
+ subject.action = Todo::MENTIONED
+
+ expect(subject).not_to be_self_assigned
+ end
+
+ it 'is false when todo is not self_added' do
+ subject.author = build(:user)
+
+ expect(subject).not_to be_self_assigned
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 9de16c41e94..0bcebc27598 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) }
@@ -1558,6 +1556,16 @@ describe User, models: true do
expect(ghost.email).to eq('ghost1@example.com')
end
end
+
+ context 'when a domain whitelist is in place' do
+ before do
+ stub_application_setting(domain_whitelist: ['gitlab.com'])
+ end
+
+ it 'creates a ghost user' do
+ expect(User.ghost).to be_persisted
+ end
+ end
end
describe '#update_two_factor_requirement' do
@@ -1631,4 +1639,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/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 2905d5b26a5..9a870b7fda1 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -1,118 +1,192 @@
require 'spec_helper'
describe IssuePolicy, models: true do
- let(:user) { create(:user) }
-
- describe '#rules' do
- context 'using a regular issue' do
- let(:project) { create(:empty_project, :public) }
- let(:issue) { create(:issue, project: project) }
- let(:policies) { described_class.abilities(user, issue).to_set }
-
- context 'with a regular user' do
- it 'includes the read_issue permission' do
- expect(policies).to include(:read_issue)
- end
-
- it 'does not include the admin_issue permission' do
- expect(policies).not_to include(:admin_issue)
- end
-
- it 'does not include the update_issue permission' do
- expect(policies).not_to include(:update_issue)
- end
- end
+ let(:guest) { create(:user) }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:group) { create(:group, :public) }
+ let(:reporter_from_group_link) { create(:user) }
+
+ def permissions(user, issue)
+ described_class.abilities(user, issue).to_set
+ end
+
+ context 'a private project' do
+ let(:non_member) { create(:user) }
+ let(:project) { create(:empty_project, :private) }
+ let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
+ let(:issue_no_assignee) { create(:issue, project: project) }
+
+ before do
+ project.team << [guest, :guest]
+ project.team << [author, :guest]
+ project.team << [assignee, :guest]
+ project.team << [reporter, :reporter]
+
+ group.add_reporter(reporter_from_group_link)
+
+ create(:project_group_link, group: group, project: project)
+ end
+
+ it 'does not allow non-members to read issues' do
+ expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows guests to read issues' do
+ expect(permissions(guest, issue)).to include(:read_issue)
+ expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
+
+ expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin issues' do
+ expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters from group links to read, update, and admin issues' do
+ expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue authors to read and update their issues' do
+ expect(permissions(author, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their issues' do
+ expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, issue)).not_to include(:admin_issue)
+
+ expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
- context 'with a user that is a project reporter' do
- before do
- project.team << [user, :reporter]
- end
+ context 'with confidential issues' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
+ let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
- it 'includes the read_issue permission' do
- expect(policies).to include(:read_issue)
- end
+ it 'does not allow non-members to read confidential issues' do
+ expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'does not allow guests to read confidential issues' do
+ expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
- it 'includes the admin_issue permission' do
- expect(policies).to include(:admin_issue)
- end
+ it 'allows reporters to read, update, and admin confidential issues' do
+ expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
- it 'includes the update_issue permission' do
- expect(policies).to include(:update_issue)
- end
+ it 'allows reporters from group links to read, update, and admin confidential issues' do
+ expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
end
- context 'with a user that is a project guest' do
- before do
- project.team << [user, :guest]
- end
+ it 'allows issue authors to read and update their confidential issues' do
+ expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
- it 'includes the read_issue permission' do
- expect(policies).to include(:read_issue)
- end
+ expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
- it 'does not include the admin_issue permission' do
- expect(policies).not_to include(:admin_issue)
- end
+ it 'allows issue assignees to read and update their confidential issues' do
+ expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
- it 'does not include the update_issue permission' do
- expect(policies).not_to include(:update_issue)
- end
+ expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
end
end
+ end
- context 'using a confidential issue' do
- let(:issue) { create(:issue, :confidential) }
+ context 'a public project' do
+ let(:project) { create(:empty_project, :public) }
+ let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
+ let(:issue_no_assignee) { create(:issue, project: project) }
- context 'with a regular user' do
- let(:policies) { described_class.abilities(user, issue).to_set }
+ before do
+ project.team << [guest, :guest]
+ project.team << [reporter, :reporter]
- it 'does not include the read_issue permission' do
- expect(policies).not_to include(:read_issue)
- end
+ group.add_reporter(reporter_from_group_link)
- it 'does not include the admin_issue permission' do
- expect(policies).not_to include(:admin_issue)
- end
+ create(:project_group_link, group: group, project: project)
+ end
- it 'does not include the update_issue permission' do
- expect(policies).not_to include(:update_issue)
- end
- end
+ it 'allows guests to read issues' do
+ expect(permissions(guest, issue)).to include(:read_issue)
+ expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
+
+ expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin issues' do
+ expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters from group links to read, update, and admin issues' do
+ expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
- context 'with a user that is a project member' do
- let(:policies) { described_class.abilities(user, issue).to_set }
+ it 'allows issue authors to read and update their issues' do
+ expect(permissions(author, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, issue)).not_to include(:admin_issue)
- before do
- issue.project.team << [user, :reporter]
- end
+ expect(permissions(author, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their issues' do
+ expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, issue)).not_to include(:admin_issue)
- it 'includes the read_issue permission' do
- expect(policies).to include(:read_issue)
- end
+ expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
- it 'includes the admin_issue permission' do
- expect(policies).to include(:admin_issue)
- end
+ context 'with confidential issues' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
+ let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
- it 'includes the update_issue permission' do
- expect(policies).to include(:update_issue)
- end
+ it 'does not allow guests to read confidential issues' do
+ expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
end
- context 'without a user' do
- let(:policies) { described_class.abilities(nil, issue).to_set }
+ it 'allows reporters to read, update, and admin confidential issues' do
+ expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporter from group links to read, update, and admin confidential issues' do
+ expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
- it 'does not include the read_issue permission' do
- expect(policies).not_to include(:read_issue)
- end
+ it 'allows issue authors to read and update their confidential issues' do
+ expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
- it 'does not include the admin_issue permission' do
- expect(policies).not_to include(:admin_issue)
- end
+ it 'allows issue assignees to read and update their confidential issues' do
+ expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
- it 'does not include the update_issue permission' do
- expect(policies).not_to include(:update_issue)
- end
+ expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
end
end
end
diff --git a/spec/policies/issues_policy_spec.rb b/spec/policies/issues_policy_spec.rb
deleted file mode 100644
index 2b7b6cad654..00000000000
--- a/spec/policies/issues_policy_spec.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-require 'spec_helper'
-
-describe IssuePolicy, models: true do
- let(:guest) { create(:user) }
- let(:author) { create(:user) }
- let(:assignee) { create(:user) }
- let(:reporter) { create(:user) }
- let(:group) { create(:group, :public) }
- let(:reporter_from_group_link) { create(:user) }
-
- def permissions(user, issue)
- IssuePolicy.abilities(user, issue).to_set
- end
-
- context 'a private project' do
- let(:non_member) { create(:user) }
- let(:project) { create(:empty_project, :private) }
- let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
- let(:issue_no_assignee) { create(:issue, project: project) }
-
- before do
- project.team << [guest, :guest]
- project.team << [author, :guest]
- project.team << [assignee, :guest]
- project.team << [reporter, :reporter]
-
- group.add_reporter(reporter_from_group_link)
-
- create(:project_group_link, group: group, project: project)
- end
-
- it 'does not allow non-members to read issues' do
- expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows guests to read issues' do
- expect(permissions(guest, issue)).to include(:read_issue)
- expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
-
- expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
- expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- it 'allows reporters to read, update, and admin issues' do
- expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporters from group links to read, update, and admin issues' do
- expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue authors to read and update their issues' do
- expect(permissions(author, issue)).to include(:read_issue, :update_issue)
- expect(permissions(author, issue)).not_to include(:admin_issue)
-
- expect(permissions(author, issue_no_assignee)).to include(:read_issue)
- expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- it 'allows issue assignees to read and update their issues' do
- expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
- expect(permissions(assignee, issue)).not_to include(:admin_issue)
-
- expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
- expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- context 'with confidential issues' do
- let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
- let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
-
- it 'does not allow non-members to read confidential issues' do
- expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'does not allow guests to read confidential issues' do
- expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporters to read, update, and admin confidential issues' do
- expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporters from group links to read, update, and admin confidential issues' do
- expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue authors to read and update their confidential issues' do
- expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
- expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
-
- expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue assignees to read and update their confidential issues' do
- expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
- expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
-
- expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
- end
- end
-
- context 'a public project' do
- let(:project) { create(:empty_project, :public) }
- let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
- let(:issue_no_assignee) { create(:issue, project: project) }
-
- before do
- project.team << [guest, :guest]
- project.team << [reporter, :reporter]
-
- group.add_reporter(reporter_from_group_link)
-
- create(:project_group_link, group: group, project: project)
- end
-
- it 'allows guests to read issues' do
- expect(permissions(guest, issue)).to include(:read_issue)
- expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
-
- expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
- expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- it 'allows reporters to read, update, and admin issues' do
- expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporters from group links to read, update, and admin issues' do
- expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue authors to read and update their issues' do
- expect(permissions(author, issue)).to include(:read_issue, :update_issue)
- expect(permissions(author, issue)).not_to include(:admin_issue)
-
- expect(permissions(author, issue_no_assignee)).to include(:read_issue)
- expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- it 'allows issue assignees to read and update their issues' do
- expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
- expect(permissions(assignee, issue)).not_to include(:admin_issue)
-
- expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
- expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
- end
-
- context 'with confidential issues' do
- let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
- let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
-
- it 'does not allow guests to read confidential issues' do
- expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporters to read, update, and admin confidential issues' do
- expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows reporter from group links to read, update, and admin confidential issues' do
- expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue authors to read and update their confidential issues' do
- expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
- expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
-
- expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
-
- it 'allows issue assignees to read and update their confidential issues' do
- expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
- expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
-
- expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
- end
- end
- end
-end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 46edbd49b28..c8eacb38e6f 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::AccessRequests, api: true do
- include ApiHelpers
-
+describe API::AccessRequests do
let(:master) { create(:user) }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index f4d4a8a2cc7..bbdef0aeb1b 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::AwardEmoji, api: true do
- include ApiHelpers
+describe API::AwardEmoji do
let(:user) { create(:user) }
let!(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 87c36639cd4..c27db716ef8 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Boards, api: true do
- include ApiHelpers
-
+describe API::Boards do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index a70f7beaae0..7eaa89837c8 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -1,9 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::Branches, api: true do
- include ApiHelpers
-
+describe API::Branches do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository, creator: user) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index 024fa66848c..67989689799 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::BroadcastMessages, api: true do
- include ApiHelpers
-
+describe API::BroadcastMessages do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index d8b3cc041a5..1233cdc64c4 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::CommitStatuses, api: true do
- include ApiHelpers
-
+describe API::CommitStatuses do
let!(:project) { create(:project, :repository) }
let(:commit) { project.repository.commit }
let(:guest) { create_user(:guest) }
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index a10d876ffad..0b0e4c2b112 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::Commits, api: true do
- include ApiHelpers
+describe API::Commits do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
@@ -599,8 +598,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/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index e1beac28dab..843e9862b0c 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::DeployKeys, api: true do
- include ApiHelpers
-
+describe API::DeployKeys do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:empty_project, creator_id: user.id) }
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index e55575ffbda..90d78d060ca 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Deployments, api: true do
- include ApiHelpers
-
+describe API::Deployments do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { deployment.environment.project }
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index f6fd567eca5..868fef65c1c 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
- include ApiHelpers
-
+describe 'doorkeeper access' do
let!(:user) { create(:user) }
let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index b54ee8e8b85..aae03c84e1f 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Environments, api: true do
- include ApiHelpers
-
+describe API::Environments do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:empty_project, :private, namespace: user.namespace) }
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 8012530f139..fa28047d49c 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Files, api: true do
- include ApiHelpers
+describe API::Files do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository, namespace: user.namespace ) }
let(:guest) { create(:user) { |u| project.add_guest(u) } }
@@ -205,7 +204,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 +298,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/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 2545da7b1db..3e27a3bee77 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Groups, api: true do
- include ApiHelpers
+describe API::Groups do
include UploadHelpers
let(:user1) { create(:user, can_create_group: false) }
diff --git a/spec/requests/api/api_internal_helpers_spec.rb b/spec/requests/api/helpers/internal_helpers_spec.rb
index f5265ea60ff..f5265ea60ff 100644
--- a/spec/requests/api/api_internal_helpers_spec.rb
+++ b/spec/requests/api/helpers/internal_helpers_spec.rb
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 988a57a80ea..06c8eb1d0b7 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::Helpers, api: true do
+describe API::Helpers do
include API::APIGuard::HelperMethods
include API::Helpers
include SentryHelper
@@ -427,6 +427,7 @@ describe API::Helpers, api: true do
context 'current_user is nil' do
before do
expect_any_instance_of(self.class).to receive(:current_user).and_return(nil)
+ allow_any_instance_of(self.class).to receive(:initial_current_user).and_return(nil)
end
it 'returns a 401 response' do
@@ -435,13 +436,38 @@ describe API::Helpers, api: true do
end
context 'current_user is present' do
+ let(:user) { build(:user) }
+
before do
- expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(User.new)
+ expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user)
+ expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user)
end
it 'does not raise an error' do
expect { authenticate! }.not_to raise_error
end
end
+
+ context 'current_user is blocked' do
+ let(:user) { build(:user, :blocked) }
+
+ before do
+ expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user)
+ end
+
+ it 'raises an error' do
+ expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user)
+
+ expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}'
+ end
+
+ it "doesn't raise an error if an admin user is impersonating a blocked user (via sudo)" do
+ admin_user = build(:user, :admin)
+
+ expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(admin_user)
+
+ expect { authenticate! }.not_to raise_error
+ end
+ end
end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 4be67df5a00..429f1a4e375 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Internal, api: true do
- include ApiHelpers
+describe API::Internal do
let(:user) { create(:user) }
let(:key) { create(:key, user: user) }
let(:project) { create(:project, :repository) }
@@ -147,10 +146,15 @@ 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
@@ -176,6 +180,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
@@ -186,6 +191,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
@@ -196,6 +202,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
@@ -206,6 +213,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
@@ -241,6 +249,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
@@ -250,6 +259,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
@@ -267,6 +277,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
@@ -276,6 +287,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
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 551aae7d701..3ca13111acb 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -1,17 +1,19 @@
require 'spec_helper'
-describe API::Issues, api: true do
- include ApiHelpers
+describe API::Issues do
include EmailHelpers
- let(:user) { create(:user) }
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:empty_project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
- let(:guest) { create(:user) }
- let(:author) { create(:author) }
- let(:assignee) { create(:assignee) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
- let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' }
let!(:closed_issue) do
@@ -44,19 +46,19 @@ describe API::Issues, api: true do
title: issue_title,
description: issue_description
end
- let!(:label) do
+ set(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
let!(:label_link) { create(:label_link, label: label, target: issue) }
- let!(:milestone) { create(:milestone, title: '1.0.0', project: project) }
- let!(:empty_milestone) do
+ set(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ set(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { URI.escape(Milestone::None.title) }
- before do
+ before(:all) do
project.team << [user, :reporter]
project.team << [guest, :guest]
end
@@ -71,6 +73,8 @@ describe API::Issues, api: true do
end
context "when authenticated" do
+ let(:first_issue) { json_response.first }
+
it "returns an array of issues" do
get api("/issues", user)
@@ -80,46 +84,46 @@ describe API::Issues, api: true do
end
it 'returns an array of closed issues' do
- get api('/issues?state=closed', user)
+ get api('/issues', user), state: :closed
expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(closed_issue.id)
+ expect(first_issue['id']).to eq(closed_issue.id)
end
it 'returns an array of opened issues' do
- get api('/issues?state=opened', user)
+ get api('/issues', user), state: :opened
expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(issue.id)
+ expect(first_issue['id']).to eq(issue.id)
end
it 'returns an array of all issues' do
- get api('/issues?state=all', user)
+ get api('/issues', user), state: :all
expect_paginated_array_response(size: 2)
- expect(json_response.first['id']).to eq(issue.id)
+ expect(first_issue['id']).to eq(issue.id)
expect(json_response.second['id']).to eq(closed_issue.id)
end
it 'returns issues matching given search string for title' do
- get api("/issues?search=#{issue.title}", user)
+ get api("/issues", user), search: issue.title
expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(issue.id)
end
it 'returns issues matching given search string for description' do
- get api("/issues?search=#{issue.description}", user)
+ get api("/issues", user), search: issue.description
expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(issue.id)
+ expect(first_issue['id']).to eq(issue.id)
end
it 'returns an array of labeled issues' do
- get api("/issues?labels=#{label.title}", user)
+ get api("/issues", user), labels: label.title
expect_paginated_array_response(size: 1)
- expect(json_response.first['labels']).to eq([label.title])
+ expect(first_issue['labels']).to eq([label.title])
end
it 'returns an array of labeled issues when all labels matches' do
@@ -136,13 +140,13 @@ describe API::Issues, api: true do
end
it 'returns an empty array if no issue matches labels' do
- get api('/issues?labels=foo,bar', user)
+ get api('/issues', user), labels: 'foo,bar'
expect_paginated_array_response(size: 0)
end
it 'returns an array of labeled issues matching given state' do
- get api("/issues?labels=#{label.title}&state=opened", user)
+ get api("/issues", user), labels: label.title, state: :opened
expect_paginated_array_response(size: 1)
expect(json_response.first['labels']).to eq([label.title])
@@ -1345,6 +1349,41 @@ describe API::Issues, api: true do
include_examples 'time tracking endpoints', 'issue'
end
+ describe 'GET :id/issues/:issue_iid/closed_by' do
+ let(:merge_request) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "closes #{issue.to_reference}")
+ end
+
+ before do
+ create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
+ end
+
+ it 'returns merge requests that will close issue on merge' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
+
+ expect_paginated_array_response(size: 1)
+ end
+
+ context 'when no merge requests will close issue' do
+ it 'returns empty array' do
+ get api("/projects/#{project.id}/issues/#{closed_issue.iid}/closed_by", user)
+
+ expect_paginated_array_response(size: 0)
+ end
+ end
+
+ it "returns 404 when issue doesn't exists" do
+ get api("/projects/#{project.id}/issues/9999/closed_by", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
def expect_paginated_array_response(size: nil)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index d8a56c02a63..decb5b91941 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Jobs, api: true do
- include ApiHelpers
-
+describe API::Jobs do
let(:user) { create(:user) }
let(:api_user) { user }
let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index 4c80987d680..ab957c72984 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Keys, api: true do
- include ApiHelpers
-
+describe API::Keys do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
@@ -34,6 +32,12 @@ describe API::Keys, api: true do
expect(json_response['user']['id']).to eq(user.id)
expect(json_response['user']['username']).to eq(user.username)
end
+
+ it "does not include the user's `is_admin` flag" do
+ get api("/keys/#{key.id}", admin)
+
+ expect(json_response['user']['is_admin']).to be_nil
+ end
end
end
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index a1adaba7b98..0c6b55c1630 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Labels, api: true do
- include ApiHelpers
-
+describe API::Labels do
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let!(:label1) { create(:label, title: 'label1', project: project) }
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index 391fc13a380..df7c91b5bc1 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Lint, api: true do
- include ApiHelpers
-
+describe API::Lint do
describe 'POST /ci/lint' do
context 'with valid .gitlab-ci.yaml content' do
let(:yaml_content) do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 84dca51801f..e095053fa03 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Members, api: true do
- include ApiHelpers
-
+describe API::Members do
let(:master) { create(:user, username: 'master_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index 79f3151ba52..d1b22179888 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -1,8 +1,6 @@
require "spec_helper"
-describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
- include ApiHelpers
-
+describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
let!(:user) { create(:user) }
let!(:merge_request) { create(:merge_request, importing: true) }
let!(:project) { merge_request.target_project }
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 61d965e8974..16e5efb2f5b 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1,16 +1,22 @@
require "spec_helper"
-describe API::MergeRequests, api: true do
- include ApiHelpers
+describe API::MergeRequests do
let(:base_time) { Time.now }
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:non_member) { create(:user) }
- let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) }
- let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) }
- let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ let(:milestone1) { create(:milestone, title: '0.9', project: project) }
+ let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
+ let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
+ let!(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: merge_request) }
before do
project.team << [user, :reporter]
@@ -20,6 +26,7 @@ describe API::MergeRequests, api: true do
context "when unauthenticated" do
it "returns authentication error" do
get api("/projects/#{project.id}/merge_requests")
+
expect(response).to have_http_status(401)
end
end
@@ -100,6 +107,63 @@ describe API::MergeRequests, api: true do
expect(response).to match_response_schema('public_api/v4/merge_requests')
end
+ it 'returns an empty array if no issue matches milestone' do
+ get api("/projects/#{project.id}/merge_requests", user), milestone: '1.0.0'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api("/projects/#{project.id}/merge_requests", user), milestone: 'foo'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of merge requests in given milestone' do
+ get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9'
+
+ expect(json_response.first['title']).to eq merge_request_closed.title
+ expect(json_response.first['id']).to eq merge_request_closed.id
+ end
+
+ it 'returns an array of merge requests matching state in milestone' do
+ get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9', state: 'closed'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request_closed.id)
+ end
+
+ it 'returns an array of labeled merge requests' do
+ get api("/projects/#{project.id}/merge_requests?labels=#{label.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled merge requests where all labels match' do
+ get api("/projects/#{project.id}/merge_requests?labels=#{label.title},foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no merge request matches labels' do
+ get api("/projects/#{project.id}/merge_requests?labels=foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
context "with ordering" do
before do
@mr_later = mr_with_later_created_and_updated_at_time
@@ -167,7 +231,7 @@ describe API::MergeRequests, api: true do
expect(json_response['created_at']).to be_present
expect(json_response['updated_at']).to be_present
expect(json_response['labels']).to eq(merge_request.label_names)
- expect(json_response['milestone']).to be_nil
+ expect(json_response['milestone']).to be_a Hash
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
expect(json_response['target_branch']).to eq(merge_request.target_branch)
@@ -370,6 +434,19 @@ describe API::MergeRequests, api: true do
expect(json_response['title']).to eq('Test merge_request')
end
+ it 'returns 422 when target project has disabled merge requests' do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ post api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test',
+ target_branch: 'master',
+ source_branch: 'markdown',
+ author: user2,
+ target_project_id: project.id
+
+ expect(response).to have_http_status(422)
+ end
+
it "returns 400 when source_branch is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
@@ -527,6 +604,18 @@ describe API::MergeRequests, api: true do
expect(json_response['merge_when_pipeline_succeeds']).to eq(true)
end
+ it "enables merge when pipeline succeeds if the pipeline is active and only_allow_merge_if_pipeline_succeeds is true" do
+ allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:active?).and_return(true)
+ project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('Test')
+ expect(json_response['merge_when_pipeline_succeeds']).to eq(true)
+ end
+
it "returns 404 for an invalid merge request IID" do
put api("/projects/#{project.id}/merge_requests/12345/merge", user)
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 598968aff70..dd74351a2b1 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Milestones, api: true do
- include ApiHelpers
+describe API::Milestones do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, namespace: user.namespace ) }
let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index da8fa06d0af..3bf16a3ae27 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Namespaces, api: true do
- include ApiHelpers
+describe API::Namespaces do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
let!(:group1) { create(:group) }
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index d8eb8ce921e..6afcd237c3c 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::Notes, api: true do
- include ApiHelpers
+describe API::Notes do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, :public, namespace: user.namespace) }
let!(:issue) { create(:issue, project: project, author: user) }
diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb
index 39d3afcb78f..f619b7e6eaf 100644
--- a/spec/requests/api/notification_settings_spec.rb
+++ b/spec/requests/api/notification_settings_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::NotificationSettings, api: true do
- include ApiHelpers
-
+describe API::NotificationSettings do
let(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: group) }
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index 367225df717..0d56e1f732e 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
- include ApiHelpers
-
+describe 'OAuth tokens' do
context 'Resource Owner Password Credentials' do
def request_oauth_token(user)
post '/oauth/token', username: user.username, password: user.password, grant_type: 'password'
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 51af999b455..762345cd41c 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Pipelines, api: true do
- include ApiHelpers
-
+describe API::Pipelines do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:project, :repository, creator: user) }
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index b1603233f9e..aee0e17a153 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::ProjectHooks, 'ProjectHooks', api: true do
- include ApiHelpers
+describe API::ProjectHooks, 'ProjectHooks' do
let(:user) { create(:user) }
let(:user3) { create(:user) }
let!(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 9e88c19b0bc..3ab1764f5c3 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
-describe API::ProjectSnippets, api: true do
- include ApiHelpers
-
+describe API::ProjectSnippets do
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 74bc4847247..cc03d7a933b 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
require 'spec_helper'
-describe API::Projects, :api do
+describe API::Projects do
include Gitlab::CurrentSettings
let(:user) { create(:user) }
@@ -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)
@@ -1078,7 +1082,9 @@ describe API::Projects, :api do
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
@@ -1086,7 +1092,9 @@ describe API::Projects, :api do
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
@@ -1094,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
@@ -1103,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
@@ -1113,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
@@ -1139,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/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 4783d011d54..1a0695615e3 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::Repositories, api: true do
- include ApiHelpers
+describe API::Repositories do
include RepoHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 409a59d6c23..be83514ed9c 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
describe API::Runner do
- include ApiHelpers
include StubGitlabCalls
let(:registration_token) { 'abcdefg123456' }
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 8a82543a830..645a5389850 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Runners, api: true do
- include ApiHelpers
-
+describe API::Runners do
let(:admin) { create(:user, :admin) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index fd334934ca5..95df3429314 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -1,8 +1,6 @@
require "spec_helper"
-describe API::Services, api: true do
- include ApiHelpers
-
+describe API::Services do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:user2) { create(:user) }
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index 393bf076616..5e77519c867 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Session, api: true do
- include ApiHelpers
-
+describe API::Session do
let(:user) { create(:user) }
describe "POST /session" do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 11b4b718e2c..2398ae6219c 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Settings, 'Settings', api: true do
- include ApiHelpers
-
+describe API::Settings, 'Settings' do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb
index 28067f8ca88..83042d0cb12 100644
--- a/spec/requests/api/sidekiq_metrics_spec.rb
+++ b/spec/requests/api/sidekiq_metrics_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::SidekiqMetrics, api: true do
- include ApiHelpers
-
+describe API::SidekiqMetrics do
let(:admin) { create(:user, :admin) }
describe 'GET sidekiq/*' do
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 5d75b47b3cd..e429cddcf6a 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
-describe API::Snippets, api: true do
- include ApiHelpers
+describe API::Snippets do
let!(:user) { create(:user) }
describe 'GET /snippets/' do
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index d1e10f12657..c7b84173570 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::SystemHooks, api: true do
- include ApiHelpers
-
+describe API::SystemHooks do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let!(:hook) { create(:system_hook, url: "http://example.com") }
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index b132d033a61..ef7d0c3ee41 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::Tags, api: true do
- include ApiHelpers
+describe API::Tags do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 2c83e119065..cb55985e3f5 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Templates, api: true do
- include ApiHelpers
-
+describe API::Templates do
context 'the Template Entity' do
before { get api('/templates/gitignores/Ruby') }
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index b789284fa8d..92533f4dfea 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Todos, api: true do
- include ApiHelpers
-
+describe API::Todos do
let(:project_1) { create(:empty_project, :test_repo) }
let(:project_2) { create(:empty_project) }
let(:author_1) { create(:user) }
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index d93a734f5b6..16ddade27d9 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::Triggers do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:trigger_token) { 'secure_token' }
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index f793c0db2f3..4919ad19833 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1,12 +1,10 @@
require 'spec_helper'
-describe API::Users, api: true do
- include ApiHelpers
-
+describe API::Users do
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 +70,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 +104,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
@@ -110,6 +135,12 @@ describe API::Users, api: true do
expect(json_response['username']).to eq(user.username)
end
+ it "does not return the user's `is_admin` flag" do
+ get api("/users/#{user.id}", user)
+
+ expect(json_response['is_admin']).to be_nil
+ end
+
it "returns a 401 if unauthenticated" do
get api("/users/9998")
expect(response).to have_http_status(401)
@@ -129,7 +160,7 @@ describe API::Users, api: true do
end
describe "POST /users" do
- before{ admin }
+ before { admin }
it "creates user" do
expect do
@@ -214,9 +245,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 +273,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 +298,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 +319,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')
@@ -372,7 +403,6 @@ describe API::Users, api: true do
it "updates admin status" do
put api("/users/#{user.id}", admin), { admin: true }
expect(response).to have_http_status(200)
- expect(json_response['is_admin']).to eq(true)
expect(user.reload.admin).to eq(true)
end
@@ -386,7 +416,6 @@ describe API::Users, api: true do
it "does not update admin status" do
put api("/users/#{admin_user.id}", admin), { can_create_group: false }
expect(response).to have_http_status(200)
- expect(json_response['is_admin']).to eq(true)
expect(admin_user.reload.admin).to eq(true)
expect(admin_user.can_create_group).to eq(false)
end
@@ -416,12 +445,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 +517,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 +609,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 +871,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 +909,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 +992,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 +1018,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 +1187,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/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb
index eeb4d128c1b..9234710f488 100644
--- a/spec/requests/api/v3/award_emoji_spec.rb
+++ b/spec/requests/api/v3/award_emoji_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::AwardEmoji, api: true do
- include ApiHelpers
-
+describe API::V3::AwardEmoji do
let(:user) { create(:user) }
let!(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb
index eb95934f354..4d786331d1b 100644
--- a/spec/requests/api/v3/boards_spec.rb
+++ b/spec/requests/api/v3/boards_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Boards, api: true do
- include ApiHelpers
-
+describe API::V3::Boards do
let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:non_member) { create(:user) }
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
index 5dcd4f21f4e..72f8fbe71fb 100644
--- a/spec/requests/api/v3/branches_spec.rb
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -1,9 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::V3::Branches, api: true do
- include ApiHelpers
-
+describe API::V3::Branches do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, :repository, creator: user) }
diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb
index 06556401a29..948cd78c177 100644
--- a/spec/requests/api/v3/broadcast_messages_spec.rb
+++ b/spec/requests/api/v3/broadcast_messages_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::BroadcastMessages, api: true do
- include ApiHelpers
-
+describe API::V3::BroadcastMessages do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index e97d2b0cee0..dc95599546c 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Builds, api: true do
- include ApiHelpers
-
+describe API::V3::Builds do
let(:user) { create(:user) }
let(:api_user) { user }
let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
index adba3a787aa..c2e8c3ae6f7 100644
--- a/spec/requests/api/v3/commits_spec.rb
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::V3::Commits, api: true do
- include ApiHelpers
+describe API::V3::Commits do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
@@ -485,8 +484,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/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb
index f5bdf408c5e..b61b2b618a6 100644
--- a/spec/requests/api/v3/deploy_keys_spec.rb
+++ b/spec/requests/api/v3/deploy_keys_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::DeployKeys, api: true do
- include ApiHelpers
-
+describe API::V3::DeployKeys do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:empty_project, creator_id: user.id) }
diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb
index 3c5ce407b32..0389a264781 100644
--- a/spec/requests/api/v3/deployments_spec.rb
+++ b/spec/requests/api/v3/deployments_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Deployments, api: true do
- include ApiHelpers
-
+describe API::V3::Deployments do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { deployment.environment.project }
@@ -26,11 +24,11 @@ describe API::Deployments, api: true do
describe 'GET /projects/:id/deployments' do
context 'as member of the project' do
it_behaves_like 'a paginated resources' do
- let(:request) { get api("/projects/#{project.id}/deployments", user) }
+ let(:request) { get v3_api("/projects/#{project.id}/deployments", user) }
end
it 'returns projects deployments' do
- get api("/projects/#{project.id}/deployments", user)
+ get v3_api("/projects/#{project.id}/deployments", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -42,7 +40,7 @@ describe API::Deployments, api: true do
context 'as non member' do
it 'returns a 404 status code' do
- get api("/projects/#{project.id}/deployments", non_member)
+ get v3_api("/projects/#{project.id}/deployments", non_member)
expect(response).to have_http_status(404)
end
@@ -52,7 +50,7 @@ describe API::Deployments, api: true do
describe 'GET /projects/:id/deployments/:deployment_id' do
context 'as a member of the project' do
it 'returns the projects deployment' do
- get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
+ get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", user)
expect(response).to have_http_status(200)
expect(json_response['sha']).to match /\A\h{40}\z/
@@ -62,7 +60,7 @@ describe API::Deployments, api: true do
context 'as non member' do
it 'returns a 404 status code' do
- get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
+ get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb
index 216192c9d34..99f35723974 100644
--- a/spec/requests/api/v3/environments_spec.rb
+++ b/spec/requests/api/v3/environments_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Environments, api: true do
- include ApiHelpers
-
+describe API::V3::Environments do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:empty_project, :private, namespace: user.namespace) }
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
index 349fd6b3415..5bcbb441979 100644
--- a/spec/requests/api/v3/files_spec.rb
+++ b/spec/requests/api/v3/files_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Files, api: true do
- include ApiHelpers
-
+describe API::V3::Files do
# I have to remove periods from the end of the name
# This happened when the user's name had a suffix (i.e. "Sr.")
# This seems to be what git does under the hood. For example, this commit:
@@ -129,7 +127,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 +227,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/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
index a71b7d4b008..2862580cc70 100644
--- a/spec/requests/api/v3/groups_spec.rb
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::V3::Groups, api: true do
- include ApiHelpers
+describe API::V3::Groups do
include UploadHelpers
let(:user1) { create(:user, can_create_group: false) }
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 91d9057075f..ef5b10a1615 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::V3::Issues, api: true do
- include ApiHelpers
+describe API::V3::Issues do
include EmailHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb
index dfac357d37c..62faa1cb129 100644
--- a/spec/requests/api/v3/labels_spec.rb
+++ b/spec/requests/api/v3/labels_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Labels, api: true do
- include ApiHelpers
-
+describe API::V3::Labels do
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let!(:label1) { create(:label, title: 'label1', project: project) }
diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb
index af1c5cff67f..623f02902b8 100644
--- a/spec/requests/api/v3/members_spec.rb
+++ b/spec/requests/api/v3/members_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Members, api: true do
- include ApiHelpers
-
+describe API::V3::Members do
let(:master) { create(:user, username: 'master_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb
index c53800eef30..8020ddab4c8 100644
--- a/spec/requests/api/v3/merge_request_diffs_spec.rb
+++ b/spec/requests/api/v3/merge_request_diffs_spec.rb
@@ -1,8 +1,6 @@
require "spec_helper"
-describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
- include ApiHelpers
-
+describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs' do
let!(:user) { create(:user) }
let!(:merge_request) { create(:merge_request, importing: true) }
let!(:project) { merge_request.target_project }
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index d73e9635c9b..f6ff96be566 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -1,7 +1,6 @@
require "spec_helper"
-describe API::MergeRequests, api: true do
- include ApiHelpers
+describe API::MergeRequests do
let(:base_time) { Time.now }
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
@@ -339,6 +338,19 @@ describe API::MergeRequests, api: true do
expect(json_response['title']).to eq('Test merge_request')
end
+ it "returns 422 when target project has disabled merge requests" do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test',
+ target_branch: "master",
+ source_branch: 'markdown',
+ author: user2,
+ target_project_id: project.id
+
+ expect(response).to have_http_status(422)
+ end
+
it "returns 400 when source_branch is missing" do
post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb
index 127c0eec881..f04efc990a7 100644
--- a/spec/requests/api/v3/milestones_spec.rb
+++ b/spec/requests/api/v3/milestones_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::V3::Milestones, api: true do
- include ApiHelpers
+describe API::V3::Milestones do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, namespace: user.namespace ) }
let!(:closed_milestone) { create(:closed_milestone, project: project) }
diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb
index ddef2d5eb04..2bae4a60931 100644
--- a/spec/requests/api/v3/notes_spec.rb
+++ b/spec/requests/api/v3/notes_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Notes, api: true do
- include ApiHelpers
-
+describe API::V3::Notes do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, :public, namespace: user.namespace) }
let!(:issue) { create(:issue, project: project, author: user) }
diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb
index 3786eb06932..e1d036ff365 100644
--- a/spec/requests/api/v3/pipelines_spec.rb
+++ b/spec/requests/api/v3/pipelines_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Pipelines, api: true do
- include ApiHelpers
-
+describe API::V3::Pipelines do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:project, :repository, creator: user) }
diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb
index a981119dc5a..a3a4c77d09d 100644
--- a/spec/requests/api/v3/project_hooks_spec.rb
+++ b/spec/requests/api/v3/project_hooks_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::ProjectHooks, 'ProjectHooks', api: true do
- include ApiHelpers
+describe API::ProjectHooks, 'ProjectHooks' do
let(:user) { create(:user) }
let(:user3) { create(:user) }
let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb
index 957a3bf97ef..365e7365fda 100644
--- a/spec/requests/api/v3/project_snippets_spec.rb
+++ b/spec/requests/api/v3/project_snippets_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
-describe API::ProjectSnippets, api: true do
- include ApiHelpers
-
+describe API::ProjectSnippets do
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index 40531fe7545..e15b90d7a9e 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
-describe API::V3::Projects, api: true do
- include ApiHelpers
+describe API::V3::Projects do
include Gitlab::CurrentSettings
let(:user) { create(:user) }
diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb
index fef6fb641fa..1a55e2a71cd 100644
--- a/spec/requests/api/v3/repositories_spec.rb
+++ b/spec/requests/api/v3/repositories_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::V3::Repositories, api: true do
- include ApiHelpers
+describe API::V3::Repositories do
include RepoHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb
index ca335ce9cf0..dbda2cf34c3 100644
--- a/spec/requests/api/v3/runners_spec.rb
+++ b/spec/requests/api/v3/runners_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Runners, api: true do
- include ApiHelpers
-
+describe API::V3::Runners do
let(:admin) { create(:user, :admin) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb
index 3a760a8f25c..3ba62de822a 100644
--- a/spec/requests/api/v3/services_spec.rb
+++ b/spec/requests/api/v3/services_spec.rb
@@ -1,8 +1,6 @@
require "spec_helper"
-describe API::V3::Services, api: true do
- include ApiHelpers
-
+describe API::V3::Services do
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/v3/settings_spec.rb b/spec/requests/api/v3/settings_spec.rb
index a9fa5adac17..41d039b7da0 100644
--- a/spec/requests/api/v3/settings_spec.rb
+++ b/spec/requests/api/v3/settings_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Settings, 'Settings', api: true do
- include ApiHelpers
-
+describe API::V3::Settings, 'Settings' do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb
index 05653bd0d51..4f02b7b1a54 100644
--- a/spec/requests/api/v3/snippets_spec.rb
+++ b/spec/requests/api/v3/snippets_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
-describe API::V3::Snippets, api: true do
- include ApiHelpers
+describe API::V3::Snippets do
let!(:user) { create(:user) }
describe 'GET /snippets/' do
diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb
index 91038977c82..72c7d14b8ba 100644
--- a/spec/requests/api/v3/system_hooks_spec.rb
+++ b/spec/requests/api/v3/system_hooks_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::SystemHooks, api: true do
- include ApiHelpers
-
+describe API::V3::SystemHooks do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let!(:hook) { create(:system_hook, url: "http://example.com") }
diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb
index 6870cfd2668..1c4b25c47c3 100644
--- a/spec/requests/api/v3/tags_spec.rb
+++ b/spec/requests/api/v3/tags_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::V3::Tags, api: true do
- include ApiHelpers
+describe API::V3::Tags do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb
index f1e554b98cc..00446c7f29c 100644
--- a/spec/requests/api/v3/templates_spec.rb
+++ b/spec/requests/api/v3/templates_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Templates, api: true do
- include ApiHelpers
-
+describe API::V3::Templates do
shared_examples_for 'the Template Entity' do |path|
before { get v3_api(path) }
diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb
index 80fa697e949..9c2c4d64257 100644
--- a/spec/requests/api/v3/todos_spec.rb
+++ b/spec/requests/api/v3/todos_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Todos, api: true do
- include ApiHelpers
-
+describe API::V3::Todos do
let(:project_1) { create(:empty_project) }
let(:project_2) { create(:empty_project) }
let(:author_1) { create(:user) }
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
index 9233e9621bf..d3de6bf13bc 100644
--- a/spec/requests/api/v3/triggers_spec.rb
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::V3::Triggers do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:trigger_token) { 'secure_token' }
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
index b38cbe74b85..e9c57f7c6c3 100644
--- a/spec/requests/api/v3/users_spec.rb
+++ b/spec/requests/api/v3/users_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::V3::Users, api: true do
- include ApiHelpers
-
+describe API::V3::Users do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
@@ -276,5 +274,11 @@ describe API::V3::Users, api: true do
expect(new_user).to be_confirmed
end
+
+ it 'does not reveal the `is_admin` flag of the user' do
+ post v3_api('/users', admin), attributes_for(:user)
+
+ expect(json_response['is_admin']).to be_nil
+ end
end
end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 0c1413119e0..63d6d3001ac 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Variables, api: true do
- include ApiHelpers
-
+describe API::Variables do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:empty_project, creator_id: user.id) }
diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb
index da1b2fda70e..8870d48bbc9 100644
--- a/spec/requests/api/version_spec.rb
+++ b/spec/requests/api/version_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe API::Version, api: true do
- include ApiHelpers
-
+describe API::Version do
describe 'GET /version' do
context 'when unauthenticated' do
it 'returns authentication error' do
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index ef30d8638dd..108f73bb965 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Ci::API::Builds do
- include ApiHelpers
-
let(:runner) { FactoryGirl.create(:ci_runner, tag_list: %w(mysql ruby)) }
let(:project) { FactoryGirl.create(:empty_project, shared_runners_enabled: false) }
let(:last_update) { nil }
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index d50cdfdc2d6..0b9733221d8 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
describe Ci::API::Runners do
- include ApiHelpers
include StubGitlabCalls
let(:registration_token) { 'abcdefg123456' }
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index 5321f8b134f..26b03c0f148 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Ci::API::Triggers do
- include ApiHelpers
-
describe 'POST /projects/:project_id/refs/:ref/trigger' do
let!(:trigger_token) { 'secure token' }
let!(:project) { create(:project, :repository, ci_id: 10) }
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 006d6a6af1c..6ca3ef18fe6 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
@@ -270,10 +279,10 @@ describe 'Git HTTP requests', lib: true do
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
- it "uploads get status 401 (no project existence information leak)" do
+ it "uploads get status 200" do
push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
- expect(response).to have_http_status(401)
+ expect(response).to have_http_status(200)
end
end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 5206634bca5..a4f85c22943 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'OpenID Connect requests' do
- include ApiHelpers
-
let(:user) { create :user }
let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id }
let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id }
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 0edbffbcd3b..33940f70b1c 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe 'cycle analytics events' do
- include ApiHelpers
-
+describe 'cycle analytics events', api: true do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, public_builds: false) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
diff --git a/spec/requests/request_profiler_spec.rb b/spec/requests/request_profiler_spec.rb
new file mode 100644
index 00000000000..51fbfecec4b
--- /dev/null
+++ b/spec/requests/request_profiler_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Request Profiler' do
+ let(:user) { create(:user) }
+
+ shared_examples 'profiling a request' do
+ before do
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+ allow(RubyProf::Profile).to receive(:profile) do |&blk|
+ blk.call
+ RubyProf::Profile.new
+ end
+ end
+
+ it 'creates a profile of the request' do
+ project = create(:project, namespace: user.namespace)
+ time = Time.now
+ path = "/#{project.path_with_namespace}"
+
+ Timecop.freeze(time) do
+ get path, nil, 'X-Profile-Token' => Gitlab::RequestProfiler.profile_token
+ end
+
+ profile_path = "#{Gitlab.config.shared.path}/tmp/requests_profiles/#{path.tr('/', '|')}_#{time.to_i}.html"
+ expect(File.exist?(profile_path)).to be true
+ end
+
+ after do
+ Gitlab::RequestProfiler.remove_all_profiles
+ end
+ end
+
+ context "when user is logged-in" do
+ before do
+ login_as(user)
+ end
+
+ include_examples 'profiling a request'
+ end
+
+ context "when user is not logged-in" do
+ include_examples 'profiling a request'
+ end
+end
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 99c44bde151..e5fc0b676af 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -71,13 +71,15 @@ describe Admin::ProjectsController, "routing" do
end
end
-# admin_hook_test GET /admin/hooks/:hook_id/test(.:format) admin/hooks#test
+# admin_hook_test GET /admin/hooks/:id/test(.:format) admin/hooks#test
# admin_hooks GET /admin/hooks(.:format) admin/hooks#index
# POST /admin/hooks(.:format) admin/hooks#create
# admin_hook DELETE /admin/hooks/:id(.:format) admin/hooks#destroy
+# PUT /admin/hooks/:id(.:format) admin/hooks#update
+# edit_admin_hook GET /admin/hooks/:id(.:format) admin/hooks#edit
describe Admin::HooksController, "routing" do
it "to #test" do
- expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', hook_id: '1')
+ expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', id: '1')
end
it "to #index" do
@@ -88,6 +90,14 @@ describe Admin::HooksController, "routing" do
expect(post("/admin/hooks")).to route_to('admin/hooks#create')
end
+ it "to #edit" do
+ expect(get("/admin/hooks/1/edit")).to route_to('admin/hooks#edit', id: '1')
+ end
+
+ it "to #update" do
+ expect(put("/admin/hooks/1")).to route_to('admin/hooks#update', id: '1')
+ end
+
it "to #destroy" do
expect(delete("/admin/hooks/1")).to route_to('admin/hooks#destroy', id: '1')
end
diff --git a/spec/routing/environments_spec.rb b/spec/routing/environments_spec.rb
index ba124de70bb..624f3c43f0a 100644
--- a/spec/routing/environments_spec.rb
+++ b/spec/routing/environments_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::EnvironmentsController, :routing do
+describe 'environments routing', :routing do
let(:project) { create(:empty_project) }
let(:environment) do
diff --git a/spec/routing/notifications_routing_spec.rb b/spec/routing/notifications_routing_spec.rb
index 24592942a96..54ed87b5520 100644
--- a/spec/routing/notifications_routing_spec.rb
+++ b/spec/routing/notifications_routing_spec.rb
@@ -1,13 +1,11 @@
require "spec_helper"
-describe Profiles::NotificationsController do
- describe "routing" do
- it "routes to #show" do
- expect(get("/profile/notifications")).to route_to("profiles/notifications#show")
- end
+describe "notifications routing" do
+ it "routes to #show" do
+ expect(get("/profile/notifications")).to route_to("profiles/notifications#show")
+ end
- it "routes to #update" do
- expect(put("/profile/notifications")).to route_to("profiles/notifications#update")
- end
+ it "routes to #update" do
+ expect(put("/profile/notifications")).to route_to("profiles/notifications#update")
end
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index a3de022d242..163df072cf6 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -340,14 +340,16 @@ describe 'project routing' do
# test_project_hook GET /:project_id/hooks/:id/test(.:format) hooks#test
# project_hooks GET /:project_id/hooks(.:format) hooks#index
# POST /:project_id/hooks(.:format) hooks#create
- # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy
+ # edit_project_hook GET /:project_id/hooks/:id/edit(.:format) hooks#edit
+ # project_hook PUT /:project_id/hooks/:id(.:format) hooks#update
+ # DELETE /:project_id/hooks/:id(.:format) hooks#destroy
describe Projects::HooksController, 'routing' do
it 'to #test' do
expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
it_behaves_like 'RESTful project resources' do
- let(:actions) { [:index, :create, :destroy] }
+ let(:actions) { [:index, :create, :destroy, :edit, :update] }
let(:controller) { 'hooks' }
end
end
diff --git a/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb
new file mode 100644
index 00000000000..07cb3fc4a2e
--- /dev/null
+++ b/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/add_column_with_default_to_large_table'
+
+describe RuboCop::Cop::Migration::AddColumnWithDefaultToLargeTable do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ described_class::LARGE_TABLES.each do |table|
+ it "registers an offense for the #{table} table" do
+ inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+ end
+
+ it 'registers no offense for non-blacklisted tables' do
+ inspect_source(cop, "add_column_with_default :table, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'outside of migration' do
+ it 'registers no offense' do
+ table = described_class::LARGE_TABLES.sample
+ inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/add_column_with_default_spec.rb b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb
index 6b9b6b19650..3723d635083 100644
--- a/spec/rubocop/cop/migration/add_column_with_default_spec.rb
+++ b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb
@@ -3,9 +3,9 @@ require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
-require_relative '../../../../rubocop/cop/migration/add_column_with_default'
+require_relative '../../../../rubocop/cop/migration/reversible_add_column_with_default'
-describe RuboCop::Cop::Migration::AddColumnWithDefault do
+describe RuboCop::Cop::Migration::ReversibleAddColumnWithDefault do
include CopHelper
subject(:cop) { described_class.new }
diff --git a/spec/serializers/analytics_generic_entity_spec.rb b/spec/serializers/analytics_issue_entity_spec.rb
index 68086216ba9..68086216ba9 100644
--- a/spec/serializers/analytics_generic_entity_spec.rb
+++ b/spec/serializers/analytics_issue_entity_spec.rb
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/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb
index c94902dbab8..3964b998084 100644
--- a/spec/serializers/status_entity_spec.rb
+++ b/spec/serializers/status_entity_spec.rb
@@ -18,6 +18,12 @@ describe StatusEntity do
it 'contains status details' do
expect(subject).to include :text, :icon, :favicon, :label, :group
expect(subject).to include :has_details, :details_path
+ expect(subject[:favicon]).to eq('/assets/ci_favicons/favicon_status_success.ico')
+ end
+
+ it 'contains a dev namespaced favicon if dev env' do
+ allow(Rails.env).to receive(:development?) { true }
+ expect(entity.as_json[:favicon]).to eq('/assets/ci_favicons/dev/favicon_status_success.ico')
end
end
end
diff --git a/spec/services/ci/expire_pipeline_cache_service_spec.rb b/spec/services/ci/expire_pipeline_cache_service_spec.rb
deleted file mode 100644
index 166c6dfc93e..00000000000
--- a/spec/services/ci/expire_pipeline_cache_service_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require 'spec_helper'
-
-describe Ci::ExpirePipelineCacheService, services: true do
- let(:user) { create(:user) }
- let(:project) { create(:empty_project) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
- subject { described_class.new(project, user) }
-
- describe '#execute' do
- it 'invalidate Etag caching for project pipelines path' do
- pipelines_path = "/#{project.full_path}/pipelines.json"
- new_mr_pipelines_path = "/#{project.full_path}/merge_requests/new.json"
-
- expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipelines_path)
- expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(new_mr_pipelines_path)
-
- subject.execute(pipeline)
- end
-
- it 'updates the cached status for a project' do
- expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline).
- with(pipeline)
-
- subject.execute(pipeline)
- 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/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index 4a4929daefc..c3b4c2176ee 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper.rb'
-class DummyService < Issues::BaseService
- include ::Issues::ResolveDiscussions
+describe Issues::ResolveDiscussions, services: true do
+ class DummyService < Issues::BaseService
+ include ::Issues::ResolveDiscussions
- def initialize(*args)
- super
- filter_resolve_discussion_params
+ def initialize(*args)
+ super
+ filter_resolve_discussion_params
+ end
end
-end
-describe DummyService, services: true do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
@@ -23,7 +23,7 @@ describe DummyService, services: true do
let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") }
describe "#merge_request_for_resolving_discussion" do
- let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
+ let(:service) { DummyService.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
it "finds the merge request" do
expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request)
@@ -43,7 +43,7 @@ describe DummyService, services: true do
describe "#discussions_to_resolve" do
it "contains a single discussion when matching merge request and discussion are passed" do
- service = described_class.new(
+ service = DummyService.new(
project,
user,
discussion_to_resolve: discussion.id,
@@ -61,7 +61,7 @@ describe DummyService, services: true do
noteable: merge_request,
project: merge_request.target_project,
line_number: 15)])
- service = described_class.new(
+ service = DummyService.new(
project,
user,
merge_request_to_resolve_discussions_of: merge_request.iid
@@ -79,7 +79,7 @@ describe DummyService, services: true do
project: merge_request.target_project,
line_number: 15,
)])
- service = described_class.new(
+ service = DummyService.new(
project,
user,
merge_request_to_resolve_discussions_of: merge_request.iid
@@ -92,7 +92,7 @@ describe DummyService, services: true do
end
it "is empty when a discussion and another merge request are passed" do
- service = described_class.new(
+ service = DummyService.new(
project,
user,
discussion_to_resolve: discussion.id,
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/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index be9f9ea2dec..6f9d1208b1d 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -261,6 +261,16 @@ describe MergeRequests::BuildService, services: true do
end
end
+ context 'upstream project has disabled merge requests' do
+ let(:upstream_project) { create(:empty_project, :merge_requests_disabled) }
+ let(:project) { create(:empty_project, forked_from_project: upstream_project) }
+ let(:commits) { Commit.decorate([commit_1], project) }
+
+ it 'sets target project correctly' do
+ expect(merge_request.target_project).to eq(project)
+ end
+ end
+
context 'target_project is set and accessible by current_user' do
let(:target_project) { create(:project, :public, :repository)}
let(:commits) { Commit.decorate([commit_1], project) }
diff --git a/spec/services/merge_requests/resolved_discussion_notification_service.rb b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
index 7ddd812e513..7ddd812e513 100644
--- a/spec/services/merge_requests/resolved_discussion_notification_service.rb
+++ b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 7a07ea618c0..033e6ecd18c 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -27,6 +27,22 @@ describe Projects::CreateService, '#execute', services: true do
end
end
+ context "admin creates project with other user's namespace_id" do
+ it 'sets the correct permissions' do
+ admin = create(:admin)
+ opts = {
+ name: 'GitLab',
+ namespace_id: user.namespace.id
+ }
+ project = create_project(admin, opts)
+
+ expect(project).to be_persisted
+ expect(project.owner).to eq(user)
+ expect(project.team.masters).to include(user, admin)
+ expect(project.namespace).to eq(user.namespace)
+ end
+ end
+
context 'group namespace' do
let(:group) do
create(:group).tap do |group|
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/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index a63281f0eab..29e65fe7ce6 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -52,7 +52,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unassign command' do
it 'populates assignee_id: nil if content contains /unassign' do
- issuable.update(assignee_id: developer.id)
+ issuable.update!(assignee_id: developer.id)
_, updates = service.execute(content, issuable)
expect(updates).to eq(assignee_id: nil)
@@ -70,7 +70,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'remove_milestone command' do
it 'populates milestone_id: nil if content contains /remove_milestone' do
- issuable.update(milestone_id: milestone.id)
+ issuable.update!(milestone_id: milestone.id)
_, updates = service.execute(content, issuable)
expect(updates).to eq(milestone_id: nil)
@@ -108,7 +108,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unlabel command' do
it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do
- issuable.update(label_ids: [inprogress.id]) # populate the label
+ issuable.update!(label_ids: [inprogress.id]) # populate the label
_, updates = service.execute(content, issuable)
expect(updates).to eq(remove_label_ids: [inprogress.id])
@@ -117,7 +117,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'multiple unlabel command' do
it 'fetches label ids and populates remove_label_ids if content contains mutiple /unlabel' do
- issuable.update(label_ids: [inprogress.id, bug.id]) # populate the label
+ issuable.update!(label_ids: [inprogress.id, bug.id]) # populate the label
_, updates = service.execute(content, issuable)
expect(updates).to eq(remove_label_ids: [inprogress.id, bug.id])
@@ -126,7 +126,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unlabel command with no argument' do
it 'populates label_ids: [] if content contains /unlabel with no arguments' do
- issuable.update(label_ids: [inprogress.id]) # populate the label
+ issuable.update!(label_ids: [inprogress.id]) # populate the label
_, updates = service.execute(content, issuable)
expect(updates).to eq(label_ids: [])
@@ -135,7 +135,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'relabel command' do
it 'populates label_ids: [] if content contains /relabel' do
- issuable.update(label_ids: [bug.id]) # populate the label
+ issuable.update!(label_ids: [bug.id]) # populate the label
inprogress # populate the label
_, updates = service.execute(content, issuable)
@@ -187,7 +187,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'remove_due_date command' do
it 'populates due_date: nil if content contains /remove_due_date' do
- issuable.update(due_date: Date.today)
+ issuable.update!(due_date: Date.today)
_, updates = service.execute(content, issuable)
expect(updates).to eq(due_date: nil)
@@ -204,7 +204,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unwip command' do
it 'returns wip_event: "unwip" if content contains /wip' do
- issuable.update(title: issuable.wip_title)
+ issuable.update!(title: issuable.wip_title)
_, updates = service.execute(content, issuable)
expect(updates).to eq(wip_event: 'unwip')
@@ -727,5 +727,75 @@ describe SlashCommands::InterpretService, services: true do
end
end
end
+
+ context '/board_move command' do
+ let(:todo) { create(:label, project: project, title: 'To Do') }
+ let(:inreview) { create(:label, project: project, title: 'In Review') }
+ let(:content) { %{/board_move ~"#{inreview.title}"} }
+
+ let!(:board) { create(:board, project: project) }
+ let!(:todo_list) { create(:list, board: board, label: todo) }
+ let!(:inreview_list) { create(:list, board: board, label: inreview) }
+ let!(:inprogress_list) { create(:list, board: board, label: inprogress) }
+
+ it 'populates remove_label_ids for all current board columns' do
+ issue.update!(label_ids: [todo.id, inprogress.id])
+
+ _, updates = service.execute(content, issue)
+
+ expect(updates[:remove_label_ids]).to match_array([todo.id, inprogress.id])
+ end
+
+ it 'populates add_label_ids with the id of the given label' do
+ _, updates = service.execute(content, issue)
+
+ expect(updates[:add_label_ids]).to eq([inreview.id])
+ end
+
+ it 'does not include the given label id in remove_label_ids' do
+ issue.update!(label_ids: [todo.id, inreview.id])
+
+ _, updates = service.execute(content, issue)
+
+ expect(updates[:remove_label_ids]).to match_array([todo.id])
+ end
+
+ it 'does not remove label ids that are not lists on the board' do
+ issue.update!(label_ids: [todo.id, bug.id])
+
+ _, updates = service.execute(content, issue)
+
+ expect(updates[:remove_label_ids]).to match_array([todo.id])
+ end
+
+ context 'if the project has multiple boards' do
+ let(:issuable) { issue }
+ before { create(:board, project: project) }
+ it_behaves_like 'empty command'
+ end
+
+ context 'if the given label does not exist' do
+ let(:issuable) { issue }
+ let(:content) { '/board_move ~"Fake Label"' }
+ it_behaves_like 'empty command'
+ end
+
+ context 'if multiple labels are given' do
+ let(:issuable) { issue }
+ let(:content) { %{/board_move ~"#{inreview.title}" ~"#{todo.title}"} }
+ it_behaves_like 'empty command'
+ end
+
+ context 'if the given label is not a list on the board' do
+ let(:issuable) { issue }
+ let(:content) { %{/board_move ~"#{bug.title}"} }
+ it_behaves_like 'empty command'
+ end
+
+ context 'if issuable is not an Issue' do
+ let(:issuable) { merge_request }
+ it_behaves_like 'empty command'
+ end
+ end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 42d63a9f9ba..75d7caf2508 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -595,7 +595,7 @@ describe SystemNoteService, services: true do
end
shared_examples 'cross project mentionable' do
- include GitlabMarkdownHelper
+ include MarkupHelper
it 'contains cross reference to new noteable' do
expect(subject.note).to include cross_project_reference(new_project, new_noteable)
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/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 e67ad8f3455..e2d5928e5b2 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -13,8 +13,9 @@ 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/
+ ENV['GITLAB_DATABASE'] == 'postgresql' &&
+ (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'
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 1a061ef069e..bb4542b1683 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -73,9 +73,15 @@ shared_examples 'discussion comments' do |resource_name|
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
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index a4713e53f63..5bbe36d9b7f 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -3,7 +3,6 @@
shared_examples 'issuable record that supports slash commands in its description and notes' do |issuable_type|
include SlashCommandsHelpers
- include WaitForAjax
let(:master) { create(:user) }
let(:assignee) { create(:user, username: 'bob') }
diff --git a/spec/support/helpers/fake_blob_helpers.rb b/spec/support/helpers/fake_blob_helpers.rb
new file mode 100644
index 00000000000..b29af732ad3
--- /dev/null
+++ b/spec/support/helpers/fake_blob_helpers.rb
@@ -0,0 +1,50 @@
+module FakeBlobHelpers
+ class FakeBlob
+ include Linguist::BlobHelper
+
+ attr_reader :path, :size, :data, :lfs_oid, :lfs_size
+
+ def initialize(path: 'file.txt', size: 1.kilobyte, data: 'foo', binary: false, lfs: nil)
+ @path = path
+ @size = size
+ @data = data
+ @binary = binary
+
+ @lfs_pointer = lfs.present?
+ if @lfs_pointer
+ @lfs_oid = SecureRandom.hex(20)
+ @lfs_size = 1.megabyte
+ end
+ end
+
+ alias_method :name, :path
+
+ def mode
+ nil
+ end
+
+ def id
+ 0
+ end
+
+ def binary?
+ @binary
+ end
+
+ def load_all_data!(repository)
+ # No-op
+ end
+
+ def lfs_pointer?
+ @lfs_pointer
+ end
+
+ def truncated?
+ false
+ end
+ end
+
+ def fake_blob(**kwargs)
+ Blob.decorate(FakeBlob.new(**kwargs), project)
+ end
+end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index 9ffb00be0b8..e6da852e728 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -84,8 +84,4 @@ module LoginHelpers
def logout_direct
page.driver.submit :delete, '/users/sign_out', {}
end
-
- def skip_ci_admin_auth
- allow_any_instance_of(Ci::Admin::ApplicationController).to receive_messages(authenticate_admin!: true)
- end
end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index dea0015f105..21a054af4e1 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -23,7 +23,7 @@ class MarkdownFeature
# Direct references ----------------------------------------------------------
def project
- @project ||= create(:project).tap do |project|
+ @project ||= create(:project, :repository).tap do |project|
project.team << [user, :master]
end
end
@@ -80,7 +80,7 @@ class MarkdownFeature
def xproject
@xproject ||= begin
group = create(:group, :nested)
- create(:project, namespace: group) do |project|
+ create(:project, :repository, namespace: group) do |project|
project.team << [user, :developer]
end
end
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index 7d238850520..3e4ca8b7ab0 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -51,7 +51,7 @@ module AccessMatchers
emulate_user(user, @membership)
visit(url)
- status_code != 404 && current_path != new_user_session_path
+ status_code == 200 && current_path != new_user_session_path
end
chain :of do |membership|
@@ -66,7 +66,7 @@ module AccessMatchers
emulate_user(user, @membership)
visit(url)
- status_code == 404 || current_path == new_user_session_path
+ [401, 404].include?(status_code) || current_path == new_user_session_path
end
chain :of do |membership|
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 60c2096a126..0b3c6169c9b 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -38,7 +38,9 @@ module TestEnv
'deleted-image-test' => '6c17798',
'wip' => 'b9238ee',
'csv' => '3dd0896',
- 'v1.1.0' => 'b83d6e3'
+ 'v1.1.0' => 'b83d6e3',
+ 'add-ipython-files' => '6d85bb69',
+ 'add-pdf-file' => 'e774ebd3'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -124,12 +126,13 @@ module TestEnv
raise "Can't clone gitaly"
end
- start_gitaly(gitaly_dir, socket_path)
+ start_gitaly(gitaly_dir)
end
- def start_gitaly(gitaly_dir, socket_path)
+ def start_gitaly(gitaly_dir)
gitaly_exec = File.join(gitaly_dir, 'gitaly')
- @gitaly_pid = spawn({ "GITALY_SOCKET_PATH" => socket_path }, gitaly_exec, [:out, :err] => '/dev/null')
+ gitaly_config = File.join(gitaly_dir, 'config.toml')
+ @gitaly_pid = spawn(gitaly_exec, gitaly_config, [:out, :err] => '/dev/null')
end
def stop_gitaly
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/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb
index 0f9dc2dee75..508de2ee8e1 100644
--- a/spec/support/wait_for_ajax.rb
+++ b/spec/support/wait_for_ajax.rb
@@ -6,10 +6,13 @@ module WaitForAjax
end
def finished_all_ajax_requests?
+ return true unless javascript_test?
+ return true if page.evaluate_script('typeof jQuery === "undefined"')
+
page.evaluate_script('jQuery.active').zero?
end
def javascript_test?
- [:selenium, :webkit, :chrome, :poltergeist].include?(Capybara.current_driver)
+ Capybara.current_driver == Capybara.javascript_driver
end
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index daea0c6bb37..df2f2ce95e6 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -230,11 +230,13 @@ describe 'gitlab:app namespace rake task' do
before do
FileUtils.mkdir('tmp/tests/default_storage')
FileUtils.mkdir('tmp/tests/custom_storage')
+ gitaly_address = Gitlab.config.repositories.storages.default.gitaly_address
storages = {
- 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') },
- 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage') }
+ 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address },
+ 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ Gitlab::GitalyClient.configure_channels
# Create the projects now, after mocking the settings but before doing the backup
project_a
@@ -350,7 +352,7 @@ describe 'gitlab:app namespace rake task' do
end
it 'name has human readable time' do
- expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_gitlab_backup.tar$/)
+ expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+(-pre)?_gitlab_backup.tar$/)
end
end
end # gitlab:app namespace
diff --git a/spec/unicorn/unicorn_spec.rb b/spec/unicorn/unicorn_spec.rb
new file mode 100644
index 00000000000..8518c047a47
--- /dev/null
+++ b/spec/unicorn/unicorn_spec.rb
@@ -0,0 +1,98 @@
+require 'fileutils'
+
+require 'excon'
+
+require 'spec_helper'
+
+describe 'Unicorn' do
+ before(:all) do
+ config_lines = File.read('config/unicorn.rb.example').split("\n")
+
+ # Remove these because they make setup harder.
+ config_lines = config_lines.reject do |line|
+ %w[
+ working_directory
+ worker_processes
+ listen
+ pid
+ stderr_path
+ stdout_path
+ ].any? { |prefix| line.start_with?(prefix) }
+ end
+
+ config_lines << "working_directory '#{Rails.root}'"
+
+ # We want to have exactly 1 worker process because that makes it
+ # predictable which process will handle our requests.
+ config_lines << 'worker_processes 1'
+
+ @socket_path = File.join(Dir.pwd, 'tmp/tests/unicorn.socket')
+ config_lines << "listen '#{@socket_path}'"
+
+ ready_file = 'tmp/tests/unicorn-worker-ready'
+ FileUtils.rm_f(ready_file)
+ after_fork_index = config_lines.index { |l| l.start_with?('after_fork') }
+ config_lines.insert(after_fork_index + 1, "File.write('#{ready_file}', Process.pid)")
+
+ config_path = 'tmp/tests/unicorn.rb'
+ File.write(config_path, config_lines.join("\n") + "\n")
+
+ cmd = %W[unicorn -E test -c #{config_path} #{Rails.root.join('config.ru')}]
+ @unicorn_master_pid = spawn(*cmd)
+ wait_unicorn_boot!(@unicorn_master_pid, ready_file)
+ WebMock.allow_net_connect!
+ end
+
+ %w[SIGQUIT SIGTERM SIGKILL].each do |signal|
+ it "has a worker that self-terminates on signal #{signal}" do
+ response = Excon.get('unix:///unicorn_test/pid', socket: @socket_path)
+ expect(response.status).to eq(200)
+
+ worker_pid = response.body.to_i
+ expect(worker_pid).to be > 0
+
+ begin
+ Excon.post('unix:///unicorn_test/kill', socket: @socket_path, body: "signal=#{signal}")
+ rescue Excon::Error::Socket
+ # The connection may be closed abruptly
+ end
+
+ expect(pid_gone?(worker_pid)).to eq(true)
+ end
+ end
+
+ after(:all) do
+ WebMock.disable_net_connect!(allow_localhost: true)
+ Process.kill('TERM', @unicorn_master_pid)
+ end
+
+ def wait_unicorn_boot!(master_pid, ready_file)
+ # Unicorn should boot in under 60 seconds so 120 seconds seems like a good timeout.
+ timeout = 120
+ timeout.times do
+ return if File.exist?(ready_file)
+ pid = Process.waitpid(master_pid, Process::WNOHANG)
+ raise "unicorn failed to boot: #{$?}" unless pid.nil?
+
+ sleep 1
+ end
+
+ raise "unicorn boot timed out after #{timeout} seconds"
+ end
+
+ def pid_gone?(pid)
+ # Worker termination should take less than a second. That makes 10
+ # seconds a generous timeout.
+ 10.times do
+ begin
+ Process.kill(0, pid)
+ rescue Errno::ESRCH
+ return true
+ end
+
+ sleep 1
+ end
+
+ false
+ end
+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/_last_commit.html.haml_spec.rb b/spec/views/projects/_last_commit.html.haml_spec.rb
new file mode 100644
index 00000000000..eea1695b171
--- /dev/null
+++ b/spec/views/projects/_last_commit.html.haml_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe 'projects/_last_commit', :view do
+ let(:project) { create(:project, :repository) }
+
+ context 'when there is a pipeline present for the commit' do
+ context 'when pipeline is blocked' do
+ let!(:pipeline) do
+ create(:ci_pipeline, :blocked, project: project,
+ sha: project.commit.id)
+ end
+
+ it 'shows correct pipeline badge' do
+ render 'projects/last_commit', commit: project.commit,
+ project: project,
+ ref: :master
+
+ expect(rendered).to have_text "blocked #{project.commit.short_id}"
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb
new file mode 100644
index 00000000000..501f90c5f9a
--- /dev/null
+++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe 'projects/blob/_viewer.html.haml', :view do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project) }
+
+ let(:viewer_class) do
+ Class.new(BlobViewer::Base) do
+ include BlobViewer::Rich
+
+ self.partial_name = 'text'
+ self.max_size = 1.megabyte
+ self.absolute_max_size = 5.megabytes
+ self.client_side = false
+ end
+ end
+
+ let(:viewer) { viewer_class.new(blob) }
+ let(:blob) { fake_blob }
+
+ before do
+ assign(:project, project)
+ assign(:blob, blob)
+ assign(:id, File.join('master', blob.path))
+
+ controller.params[:controller] = 'projects/blob'
+ controller.params[:action] = 'show'
+ controller.params[:namespace_id] = project.namespace.to_param
+ controller.params[:project_id] = project.to_param
+ controller.params[:id] = File.join('master', blob.path)
+ end
+
+ def render_view
+ render partial: 'projects/blob/viewer', locals: { viewer: viewer }
+ end
+
+ context 'when the viewer is server side' do
+ before do
+ viewer_class.client_side = false
+ end
+
+ context 'when there is no render error' do
+ it 'adds a URL to the blob viewer element' do
+ render_view
+
+ expect(rendered).to have_css('.blob-viewer[data-url]')
+ end
+
+ it 'displays a spinner' do
+ render_view
+
+ expect(rendered).to have_css('i[aria-label="Loading content"]')
+ end
+ end
+
+ context 'when there is a render error' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'renders the error' do
+ render_view
+
+ expect(view).to render_template('projects/blob/_render_error')
+ end
+ end
+ end
+
+ context 'when the viewer is client side' do
+ before do
+ viewer_class.client_side = true
+ end
+
+ context 'when there is no render error' do
+ it 'prepares the viewer' do
+ expect(viewer).to receive(:prepare!)
+
+ render_view
+ end
+
+ it 'renders the viewer' do
+ render_view
+
+ expect(view).to render_template('projects/blob/viewers/_text')
+ end
+ end
+
+ context 'when there is a render error' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'renders the error' do
+ render_view
+
+ expect(view).to render_template('projects/blob/_render_error')
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index cec87dcecc8..ab120929c6c 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-describe 'projects/commit/_commit_box.html.haml' do
- include Devise::Test::ControllerHelpers
-
+describe 'projects/commit/_commit_box.html.haml', :view do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -18,14 +16,32 @@ describe 'projects/commit/_commit_box.html.haml' do
expect(rendered).to have_text("#{Commit.truncate_sha(project.commit.sha)}")
end
- it 'shows the last pipeline that ran for the commit' do
- create(:ci_pipeline, project: project, sha: project.commit.id, status: 'success')
- create(:ci_pipeline, project: project, sha: project.commit.id, status: 'canceled')
- third_pipeline = create(:ci_pipeline, project: project, sha: project.commit.id, status: 'failed')
+ context 'when there is a pipeline present' do
+ context 'when there are multiple pipelines for a commit' do
+ it 'shows the last pipeline' do
+ create(:ci_pipeline, project: project, sha: project.commit.id, status: 'success')
+ create(:ci_pipeline, project: project, sha: project.commit.id, status: 'canceled')
+ third_pipeline = create(:ci_pipeline, project: project, sha: project.commit.id, status: 'failed')
- render
+ render
+
+ expect(rendered).to have_text("Pipeline ##{third_pipeline.id} failed")
+ end
+ end
- expect(rendered).to have_text("Pipeline ##{third_pipeline.id} failed")
+ context 'when pipeline for the commit is blocked' do
+ let!(:pipeline) do
+ create(:ci_pipeline, :blocked, project: project,
+ sha: project.commit.id)
+ end
+
+ it 'shows correct pipeline description' do
+ render
+
+ expect(rendered).to have_text "Pipeline ##{pipeline.id} " \
+ 'waiting for manual action'
+ end
+ end
end
context 'viewing a commit' do
diff --git a/spec/workers/expire_build_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
index d202b3de77e..1d8da68883b 100644
--- a/spec/workers/expire_build_instance_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
@@ -34,12 +34,14 @@ describe ExpireBuildInstanceArtifactsWorker do
context 'when associated project was removed' do
let(:build) do
create(:ci_build, :artifacts, artifacts_expiry) do |build|
- build.project.delete
+ build.project.pending_delete = true
end
end
it 'does not remove artifacts' do
- expect(build.reload.artifacts_file.exists?).to be_truthy
+ expect do
+ build.reload.artifacts_file
+ end.not_to raise_error
end
end
end
diff --git a/spec/workers/expire_pipeline_cache_worker_spec.rb b/spec/workers/expire_pipeline_cache_worker_spec.rb
new file mode 100644
index 00000000000..ceba604dea2
--- /dev/null
+++ b/spec/workers/expire_pipeline_cache_worker_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe ExpirePipelineCacheWorker do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'invalidates Etag caching for project pipelines path' do
+ pipelines_path = "/#{project.full_path}/pipelines.json"
+ new_mr_pipelines_path = "/#{project.full_path}/merge_requests/new.json"
+
+ expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipelines_path)
+ expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(new_mr_pipelines_path)
+
+ subject.perform(pipeline.id)
+ end
+
+ it 'invalidates Etag caching for merge request pipelines if pipeline runs on any commit of that source branch' do
+ pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master')
+ merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+ merge_request_pipelines_path = "/#{project.full_path}/merge_requests/#{merge_request.iid}/pipelines.json"
+
+ allow_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch)
+ expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(merge_request_pipelines_path)
+
+ subject.perform(pipeline.id)
+ end
+
+ it "doesn't do anything if the pipeline not exist" do
+ expect_any_instance_of(Gitlab::EtagCaching::Store).not_to receive(:touch)
+
+ subject.perform(617748)
+ end
+
+ it 'updates the cached status for a project' do
+ expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline).
+ with(pipeline)
+
+ subject.perform(pipeline.id)
+ end
+ 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/pipeline_proccess_worker_spec.rb b/spec/workers/pipeline_process_worker_spec.rb
index 86e9d7f6684..86e9d7f6684 100644
--- a/spec/workers/pipeline_proccess_worker_spec.rb
+++ b/spec/workers/pipeline_process_worker_spec.rb
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/Dockerfile/CONTRIBUTING.md b/vendor/Dockerfile/CONTRIBUTING.md
new file mode 100644
index 00000000000..91b92eafa1b
--- /dev/null
+++ b/vendor/Dockerfile/CONTRIBUTING.md
@@ -0,0 +1,5 @@
+The canonical repository for `Dockerfile` templates is
+https://gitlab.com/gitlab-org/Dockerfile.
+
+GitLab only mirrors the templates. Please submit your merge requests to
+https://gitlab.com/gitlab-org/Dockerfile.
diff --git a/vendor/dockerfile/HTTPdDockerfile b/vendor/Dockerfile/HTTPd.Dockerfile
index 2f05427323c..2f05427323c 100644
--- a/vendor/dockerfile/HTTPdDockerfile
+++ b/vendor/Dockerfile/HTTPd.Dockerfile
diff --git a/vendor/Dockerfile/LICENSE b/vendor/Dockerfile/LICENSE
new file mode 100644
index 00000000000..d6c93c6fcf7
--- /dev/null
+++ b/vendor/Dockerfile/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016-2017 GitLab.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/Dockerfile/PHP.Dockerfile b/vendor/Dockerfile/PHP.Dockerfile
new file mode 100644
index 00000000000..6b098efcd85
--- /dev/null
+++ b/vendor/Dockerfile/PHP.Dockerfile
@@ -0,0 +1,14 @@
+FROM php:7.0-apache
+
+# Customize any core extensions here
+#RUN apt-get update && apt-get install -y \
+# libfreetype6-dev \
+# libjpeg62-turbo-dev \
+# libmcrypt-dev \
+# libpng12-dev \
+# && docker-php-ext-install -j$(nproc) iconv mcrypt \
+# && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
+# && docker-php-ext-install -j$(nproc) gd
+
+COPY config/php.ini /usr/local/etc/php/
+COPY src/ /var/www/html/
diff --git a/vendor/Dockerfile/Python2.Dockerfile b/vendor/Dockerfile/Python2.Dockerfile
new file mode 100644
index 00000000000..c9a03584d40
--- /dev/null
+++ b/vendor/Dockerfile/Python2.Dockerfile
@@ -0,0 +1,11 @@
+FROM python:2.7
+
+RUN mkdir -p /usr/src/app
+WORKDIR /usr/src/app
+
+COPY requirements.txt /usr/src/app/
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . /usr/src/app
+
+CMD ["python", "app.py"]
diff --git a/vendor/assets/javascripts/notebooklab.js b/vendor/assets/javascripts/notebooklab.js
deleted file mode 100644
index b8cfdc53b48..00000000000
--- a/vendor/assets/javascripts/notebooklab.js
+++ /dev/null
@@ -1,5733 +0,0 @@
-(function webpackUniversalModuleDefinition(root, factory) {
- if(typeof exports === 'object' && typeof module === 'object')
- module.exports = factory();
- else if(typeof define === 'function' && define.amd)
- define("NotebookLab", [], factory);
- else if(typeof exports === 'object')
- exports["NotebookLab"] = factory();
- else
- root["NotebookLab"] = factory();
-})(this, function() {
-return /******/ (function(modules) { // webpackBootstrap
-/******/ // The module cache
-/******/ var installedModules = {};
-/******/
-/******/ // The require function
-/******/ function __webpack_require__(moduleId) {
-/******/
-/******/ // Check if module is in cache
-/******/ if(installedModules[moduleId])
-/******/ return installedModules[moduleId].exports;
-/******/
-/******/ // Create a new module (and put it into the cache)
-/******/ var module = installedModules[moduleId] = {
-/******/ i: moduleId,
-/******/ l: false,
-/******/ exports: {}
-/******/ };
-/******/
-/******/ // Execute the module function
-/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
-/******/
-/******/ // Flag the module as loaded
-/******/ module.l = true;
-/******/
-/******/ // Return the exports of the module
-/******/ return module.exports;
-/******/ }
-/******/
-/******/
-/******/ // expose the modules object (__webpack_modules__)
-/******/ __webpack_require__.m = modules;
-/******/
-/******/ // expose the module cache
-/******/ __webpack_require__.c = installedModules;
-/******/
-/******/ // identity function for calling harmony imports with the correct context
-/******/ __webpack_require__.i = function(value) { return value; };
-/******/
-/******/ // define getter function for harmony exports
-/******/ __webpack_require__.d = function(exports, name, getter) {
-/******/ if(!__webpack_require__.o(exports, name)) {
-/******/ Object.defineProperty(exports, name, {
-/******/ configurable: false,
-/******/ enumerable: true,
-/******/ get: getter
-/******/ });
-/******/ }
-/******/ };
-/******/
-/******/ // getDefaultExport function for compatibility with non-harmony modules
-/******/ __webpack_require__.n = function(module) {
-/******/ var getter = module && module.__esModule ?
-/******/ function getDefault() { return module['default']; } :
-/******/ function getModuleExports() { return module; };
-/******/ __webpack_require__.d(getter, 'a', getter);
-/******/ return getter;
-/******/ };
-/******/
-/******/ // Object.prototype.hasOwnProperty.call
-/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
-/******/
-/******/ // __webpack_public_path__
-/******/ __webpack_require__.p = "";
-/******/
-/******/ // Load entry module and return exports
-/******/ return __webpack_require__(__webpack_require__.s = 47);
-/******/ })
-/************************************************************************/
-/******/ ([
-/* 0 */
-/***/ (function(module, exports) {
-
-// this module is a runtime utility for cleaner component module output and will
-// be included in the final webpack user bundle
-
-module.exports = function normalizeComponent (
- rawScriptExports,
- compiledTemplate,
- scopeId,
- cssModules
-) {
- var esModule
- var scriptExports = rawScriptExports = rawScriptExports || {}
-
- // ES6 modules interop
- var type = typeof rawScriptExports.default
- if (type === 'object' || type === 'function') {
- esModule = rawScriptExports
- scriptExports = rawScriptExports.default
- }
-
- // Vue.extend constructor export interop
- var options = typeof scriptExports === 'function'
- ? scriptExports.options
- : scriptExports
-
- // render functions
- if (compiledTemplate) {
- options.render = compiledTemplate.render
- options.staticRenderFns = compiledTemplate.staticRenderFns
- }
-
- // scopedId
- if (scopeId) {
- options._scopeId = scopeId
- }
-
- // inject cssModules
- if (cssModules) {
- var computed = Object.create(options.computed || null)
- Object.keys(cssModules).forEach(function (key) {
- var module = cssModules[key]
- computed[key] = function () { return module }
- })
- options.computed = computed
- }
-
- return {
- esModule: esModule,
- exports: scriptExports,
- options: options
- }
-}
-
-
-/***/ }),
-/* 1 */
-/***/ (function(module, exports, __webpack_require__) {
-
-/* WEBPACK VAR INJECTION */(function(Buffer) {/*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Tobias Koppers @sokra
-*/
-// css base code, injected by the css-loader
-module.exports = function(useSourceMap) {
- var list = [];
-
- // return the list of modules as css string
- list.toString = function toString() {
- return this.map(function (item) {
- var content = cssWithMappingToString(item, useSourceMap);
- if(item[2]) {
- return "@media " + item[2] + "{" + content + "}";
- } else {
- return content;
- }
- }).join("");
- };
-
- // import a list of modules into the list
- list.i = function(modules, mediaQuery) {
- if(typeof modules === "string")
- modules = [[null, modules, ""]];
- var alreadyImportedModules = {};
- for(var i = 0; i < this.length; i++) {
- var id = this[i][0];
- if(typeof id === "number")
- alreadyImportedModules[id] = true;
- }
- for(i = 0; i < modules.length; i++) {
- var item = modules[i];
- // skip already imported module
- // this implementation is not 100% perfect for weird media query combinations
- // when a module is imported multiple times with different media queries.
- // I hope this will never occur (Hey this way we have smaller bundles)
- if(typeof item[0] !== "number" || !alreadyImportedModules[item[0]]) {
- if(mediaQuery && !item[2]) {
- item[2] = mediaQuery;
- } else if(mediaQuery) {
- item[2] = "(" + item[2] + ") and (" + mediaQuery + ")";
- }
- list.push(item);
- }
- }
- };
- return list;
-};
-
-function cssWithMappingToString(item, useSourceMap) {
- var content = item[1] || '';
- var cssMapping = item[3];
- if (!cssMapping) {
- return content;
- }
-
- if (useSourceMap) {
- var sourceMapping = toComment(cssMapping);
- var sourceURLs = cssMapping.sources.map(function (source) {
- return '/*# sourceURL=' + cssMapping.sourceRoot + source + ' */'
- });
-
- return [content].concat(sourceURLs).concat([sourceMapping]).join('\n');
- }
-
- return [content].join('\n');
-}
-
-// Adapted from convert-source-map (MIT)
-function toComment(sourceMap) {
- var base64 = new Buffer(JSON.stringify(sourceMap)).toString('base64');
- var data = 'sourceMappingURL=data:application/json;charset=utf-8;base64,' + base64;
-
- return '/*# ' + data + ' */';
-}
-
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(18).Buffer))
-
-/***/ }),
-/* 2 */
-/***/ (function(module, exports, __webpack_require__) {
-
-
-/* styles */
-__webpack_require__(44)
-
-var Component = __webpack_require__(0)(
- /* script */
- __webpack_require__(13),
- /* template */
- __webpack_require__(39),
- /* scopeId */
- "data-v-4f6bf458",
- /* cssModules */
- null
-)
-
-module.exports = Component.exports
-
-
-/***/ }),
-/* 3 */
-/***/ (function(module, exports, __webpack_require__) {
-
-/*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Tobias Koppers @sokra
- Modified by Evan You @yyx990803
-*/
-
-var hasDocument = typeof document !== 'undefined'
-
-if (typeof DEBUG !== 'undefined' && DEBUG) {
- if (!hasDocument) {
- throw new Error(
- 'vue-style-loader cannot be used in a non-browser environment. ' +
- "Use { target: 'node' } in your Webpack config to indicate a server-rendering environment."
- ) }
-}
-
-var listToStyles = __webpack_require__(46)
-
-/*
-type StyleObject = {
- id: number;
- parts: Array<StyleObjectPart>
-}
-
-type StyleObjectPart = {
- css: string;
- media: string;
- sourceMap: ?string
-}
-*/
-
-var stylesInDom = {/*
- [id: number]: {
- id: number,
- refs: number,
- parts: Array<(obj?: StyleObjectPart) => void>
- }
-*/}
-
-var head = hasDocument && (document.head || document.getElementsByTagName('head')[0])
-var singletonElement = null
-var singletonCounter = 0
-var isProduction = false
-var noop = function () {}
-
-// Force single-tag solution on IE6-9, which has a hard limit on the # of <style>
-// tags it will allow on a page
-var isOldIE = typeof navigator !== 'undefined' && /msie [6-9]\b/.test(navigator.userAgent.toLowerCase())
-
-module.exports = function (parentId, list, _isProduction) {
- isProduction = _isProduction
-
- var styles = listToStyles(parentId, list)
- addStylesToDom(styles)
-
- return function update (newList) {
- var mayRemove = []
- for (var i = 0; i < styles.length; i++) {
- var item = styles[i]
- var domStyle = stylesInDom[item.id]
- domStyle.refs--
- mayRemove.push(domStyle)
- }
- if (newList) {
- styles = listToStyles(parentId, newList)
- addStylesToDom(styles)
- } else {
- styles = []
- }
- for (var i = 0; i < mayRemove.length; i++) {
- var domStyle = mayRemove[i]
- if (domStyle.refs === 0) {
- for (var j = 0; j < domStyle.parts.length; j++) {
- domStyle.parts[j]()
- }
- delete stylesInDom[domStyle.id]
- }
- }
- }
-}
-
-function addStylesToDom (styles /* Array<StyleObject> */) {
- for (var i = 0; i < styles.length; i++) {
- var item = styles[i]
- var domStyle = stylesInDom[item.id]
- if (domStyle) {
- domStyle.refs++
- for (var j = 0; j < domStyle.parts.length; j++) {
- domStyle.parts[j](item.parts[j])
- }
- for (; j < item.parts.length; j++) {
- domStyle.parts.push(addStyle(item.parts[j]))
- }
- if (domStyle.parts.length > item.parts.length) {
- domStyle.parts.length = item.parts.length
- }
- } else {
- var parts = []
- for (var j = 0; j < item.parts.length; j++) {
- parts.push(addStyle(item.parts[j]))
- }
- stylesInDom[item.id] = { id: item.id, refs: 1, parts: parts }
- }
- }
-}
-
-function createStyleElement () {
- var styleElement = document.createElement('style')
- styleElement.type = 'text/css'
- head.appendChild(styleElement)
- return styleElement
-}
-
-function addStyle (obj /* StyleObjectPart */) {
- var update, remove
- var styleElement = document.querySelector('style[data-vue-ssr-id~="' + obj.id + '"]')
-
- if (styleElement) {
- if (isProduction) {
- // has SSR styles and in production mode.
- // simply do nothing.
- return noop
- } else {
- // has SSR styles but in dev mode.
- // for some reason Chrome can't handle source map in server-rendered
- // style tags - source maps in <style> only works if the style tag is
- // created and inserted dynamically. So we remove the server rendered
- // styles and inject new ones.
- styleElement.parentNode.removeChild(styleElement)
- }
- }
-
- if (isOldIE) {
- // use singleton mode for IE9.
- var styleIndex = singletonCounter++
- styleElement = singletonElement || (singletonElement = createStyleElement())
- update = applyToSingletonTag.bind(null, styleElement, styleIndex, false)
- remove = applyToSingletonTag.bind(null, styleElement, styleIndex, true)
- } else {
- // use multi-style-tag mode in all other cases
- styleElement = createStyleElement()
- update = applyToTag.bind(null, styleElement)
- remove = function () {
- styleElement.parentNode.removeChild(styleElement)
- }
- }
-
- update(obj)
-
- return function updateStyle (newObj /* StyleObjectPart */) {
- if (newObj) {
- if (newObj.css === obj.css &&
- newObj.media === obj.media &&
- newObj.sourceMap === obj.sourceMap) {
- return
- }
- update(obj = newObj)
- } else {
- remove()
- }
- }
-}
-
-var replaceText = (function () {
- var textStore = []
-
- return function (index, replacement) {
- textStore[index] = replacement
- return textStore.filter(Boolean).join('\n')
- }
-})()
-
-function applyToSingletonTag (styleElement, index, remove, obj) {
- var css = remove ? '' : obj.css
-
- if (styleElement.styleSheet) {
- styleElement.styleSheet.cssText = replaceText(index, css)
- } else {
- var cssNode = document.createTextNode(css)
- var childNodes = styleElement.childNodes
- if (childNodes[index]) styleElement.removeChild(childNodes[index])
- if (childNodes.length) {
- styleElement.insertBefore(cssNode, childNodes[index])
- } else {
- styleElement.appendChild(cssNode)
- }
- }
-}
-
-function applyToTag (styleElement, obj) {
- var css = obj.css
- var media = obj.media
- var sourceMap = obj.sourceMap
-
- if (media) {
- styleElement.setAttribute('media', media)
- }
-
- if (sourceMap) {
- // https://developer.chrome.com/devtools/docs/javascript-debugging
- // this makes source maps inside style tags work properly in Chrome
- css += '\n/*# sourceURL=' + sourceMap.sources[0] + ' */'
- // http://stackoverflow.com/a/26603875
- css += '\n/*# sourceMappingURL=data:application/json;base64,' + btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))) + ' */'
- }
-
- if (styleElement.styleSheet) {
- styleElement.styleSheet.cssText = css
- } else {
- while (styleElement.firstChild) {
- styleElement.removeChild(styleElement.firstChild)
- }
- styleElement.appendChild(document.createTextNode(css))
- }
-}
-
-
-/***/ }),
-/* 4 */
-/***/ (function(module, exports) {
-
-var g;
-
-// This works in non-strict mode
-g = (function() {
- return this;
-})();
-
-try {
- // This works if eval is allowed (see CSP)
- g = g || Function("return this")() || (1,eval)("this");
-} catch(e) {
- // This works if the window reference is available
- if(typeof window === "object")
- g = window;
-}
-
-// g can still be undefined, but nothing to do about it...
-// We return undefined, instead of nothing here, so it's
-// easier to handle this case. if(!global) { ...}
-
-module.exports = g;
-
-
-/***/ }),
-/* 5 */
-/***/ (function(module, exports, __webpack_require__) {
-
-var Component = __webpack_require__(0)(
- /* script */
- __webpack_require__(8),
- /* template */
- __webpack_require__(41),
- /* scopeId */
- null,
- /* cssModules */
- null
-)
-
-module.exports = Component.exports
-
-
-/***/ }),
-/* 6 */
-/***/ (function(module, exports, __webpack_require__) {
-
-
-/* styles */
-__webpack_require__(43)
-
-var Component = __webpack_require__(0)(
- /* script */
- __webpack_require__(14),
- /* template */
- __webpack_require__(38),
- /* scopeId */
- null,
- /* cssModules */
- null
-)
-
-module.exports = Component.exports
-
-
-/***/ }),
-/* 7 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _index = __webpack_require__(5);
-
-var _index2 = _interopRequireDefault(_index);
-
-var _index3 = __webpack_require__(33);
-
-var _index4 = _interopRequireDefault(_index3);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-
-exports.default = {
- components: {
- 'code-cell': _index2.default,
- 'output-cell': _index4.default
- },
- props: {
- cell: {
- type: Object,
- required: true
- },
- codeCssClass: {
- type: String,
- required: false,
- default: ''
- }
- },
- computed: {
- rawInputCode: function rawInputCode() {
- if (this.cell.source) {
- return this.cell.source.join('');
- }
-
- return '';
- },
- hasOutput: function hasOutput() {
- return this.cell.outputs.length;
- },
- output: function output() {
- return this.cell.outputs[0];
- }
- }
-};
-
-/***/ }),
-/* 8 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _highlight = __webpack_require__(16);
-
-var _highlight2 = _interopRequireDefault(_highlight);
-
-var _prompt = __webpack_require__(2);
-
-var _prompt2 = _interopRequireDefault(_prompt);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-
-exports.default = {
- components: {
- prompt: _prompt2.default
- },
- props: {
- count: {
- type: Number,
- required: false,
- default: 0
- },
- codeCssClass: {
- type: String,
- required: false,
- default: ''
- },
- type: {
- type: String,
- required: true
- },
- rawCode: {
- type: String,
- required: true
- }
- },
- computed: {
- code: function code() {
- return this.rawCode;
- },
- promptType: function promptType() {
- var type = this.type.split('put')[0];
-
- return type.charAt(0).toUpperCase() + type.slice(1);
- }
- },
- mounted: function mounted() {
- _highlight2.default.highlightElement(this.$refs.code);
- }
-};
-
-/***/ }),
-/* 9 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _marked = __webpack_require__(25);
-
-var _marked2 = _interopRequireDefault(_marked);
-
-var _prompt = __webpack_require__(2);
-
-var _prompt2 = _interopRequireDefault(_prompt);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-//
-//
-//
-//
-//
-//
-//
-
-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
- },
- props: {
- cell: {
- type: Object,
- required: true
- }
- },
- computed: {
- markdown: function markdown() {
- return (0, _marked2.default)(this.cell.source.join(''));
- }
- }
-};
-
-/***/ }),
-/* 10 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _prompt = __webpack_require__(2);
-
-var _prompt2 = _interopRequireDefault(_prompt);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-exports.default = {
- props: {
- rawCode: {
- type: String,
- required: true
- }
- },
- components: {
- prompt: _prompt2.default
- }
-}; //
-//
-//
-//
-//
-//
-//
-
-/***/ }),
-/* 11 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _prompt = __webpack_require__(2);
-
-var _prompt2 = _interopRequireDefault(_prompt);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-exports.default = {
- props: {
- outputType: {
- type: String,
- required: true
- },
- rawCode: {
- type: String,
- required: true
- }
- },
- components: {
- prompt: _prompt2.default
- }
-}; //
-//
-//
-//
-//
-//
-//
-//
-
-/***/ }),
-/* 12 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; //
-//
-//
-//
-//
-//
-//
-//
-//
-
-var _index = __webpack_require__(5);
-
-var _index2 = _interopRequireDefault(_index);
-
-var _html = __webpack_require__(31);
-
-var _html2 = _interopRequireDefault(_html);
-
-var _image = __webpack_require__(32);
-
-var _image2 = _interopRequireDefault(_image);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-exports.default = {
- props: {
- codeCssClass: {
- type: String,
- required: false,
- default: ''
- },
- count: {
- type: Number,
- required: false,
- default: 0
- },
- output: {
- type: Object,
- requred: true
- }
- },
- components: {
- 'code-cell': _index2.default,
- 'html-output': _html2.default,
- 'image-output': _image2.default
- },
- data: function data() {
- return {
- outputType: ''
- };
- },
-
- computed: {
- componentName: function componentName() {
- if (this.output.text) {
- return 'code-cell';
- } else if (this.output.data['image/png']) {
- this.outputType = 'image/png';
-
- return 'image-output';
- } else if (this.output.data['text/html']) {
- this.outputType = 'text/html';
-
- return 'html-output';
- } else if (this.output.data['image/svg+xml']) {
- this.outputType = 'image/svg+xml';
-
- return 'html-output';
- }
-
- this.outputType = 'text/plain';
- return 'code-cell';
- },
- rawCode: function rawCode() {
- if (this.output.text) {
- return this.output.text.join('');
- }
-
- return this.dataForType(this.outputType);
- }
- },
- methods: {
- dataForType: function dataForType(type) {
- var data = this.output.data[type];
-
- if ((typeof data === 'undefined' ? 'undefined' : _typeof(data)) === 'object') {
- data = data.join('');
- }
-
- return data;
- }
- }
-};
-
-/***/ }),
-/* 13 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-//
-//
-//
-//
-//
-//
-//
-//
-
-exports.default = {
- props: {
- type: {
- type: String,
- required: false
- },
- count: {
- type: Number,
- required: false
- }
- }
-};
-
-/***/ }),
-/* 14 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _cells = __webpack_require__(15);
-
-exports.default = {
- components: {
- 'code-cell': _cells.CodeCell,
- 'markdown-cell': _cells.MarkdownCell
- },
- props: {
- notebook: {
- type: Object,
- required: true
- },
- codeCssClass: {
- type: String,
- required: false,
- default: ''
- }
- },
- methods: {
- cellType: function cellType(type) {
- return type + '-cell';
- }
- },
- computed: {
- cells: function cells() {
- if (this.notebook.worksheets) {
- var data = {
- cells: []
- };
-
- return this.notebook.worksheets.reduce(function (cellData, sheet) {
- var cellDataCopy = cellData;
- cellDataCopy.cells = cellDataCopy.cells.concat(sheet.cells);
- return cellDataCopy;
- }, data).cells;
- }
-
- return this.notebook.cells;
- },
- hasNotebook: function hasNotebook() {
- return Object.keys(this.notebook).length;
- }
- }
-}; //
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-
-/***/ }),
-/* 15 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _markdown = __webpack_require__(30);
-
-Object.defineProperty(exports, 'MarkdownCell', {
- enumerable: true,
- get: function get() {
- return _interopRequireDefault(_markdown).default;
- }
-});
-
-var _code = __webpack_require__(29);
-
-Object.defineProperty(exports, 'CodeCell', {
- enumerable: true,
- get: function get() {
- return _interopRequireDefault(_code).default;
- }
-});
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-/***/ }),
-/* 16 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _prismjs = __webpack_require__(28);
-
-var _prismjs2 = _interopRequireDefault(_prismjs);
-
-__webpack_require__(26);
-
-__webpack_require__(27);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-_prismjs2.default.plugins.customClass.map({
- comment: 'c',
- error: 'err',
- operator: 'o',
- constant: 'kc',
- namespace: 'kn',
- keyword: 'k',
- string: 's',
- number: 'm',
- 'attr-name': 'na',
- builtin: 'nb',
- entity: 'ni',
- function: 'nf',
- tag: 'nt',
- variable: 'nv'
-});
-
-exports.default = _prismjs2.default;
-
-/***/ }),
-/* 17 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-exports.byteLength = byteLength
-exports.toByteArray = toByteArray
-exports.fromByteArray = fromByteArray
-
-var lookup = []
-var revLookup = []
-var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array
-
-var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
-for (var i = 0, len = code.length; i < len; ++i) {
- lookup[i] = code[i]
- revLookup[code.charCodeAt(i)] = i
-}
-
-revLookup['-'.charCodeAt(0)] = 62
-revLookup['_'.charCodeAt(0)] = 63
-
-function placeHoldersCount (b64) {
- var len = b64.length
- if (len % 4 > 0) {
- throw new Error('Invalid string. Length must be a multiple of 4')
- }
-
- // the number of equal signs (place holders)
- // if there are two placeholders, than the two characters before it
- // represent one byte
- // if there is only one, then the three characters before it represent 2 bytes
- // this is just a cheap hack to not do indexOf twice
- return b64[len - 2] === '=' ? 2 : b64[len - 1] === '=' ? 1 : 0
-}
-
-function byteLength (b64) {
- // base64 is 4/3 + up to two characters of the original data
- return b64.length * 3 / 4 - placeHoldersCount(b64)
-}
-
-function toByteArray (b64) {
- var i, j, l, tmp, placeHolders, arr
- var len = b64.length
- placeHolders = placeHoldersCount(b64)
-
- arr = new Arr(len * 3 / 4 - placeHolders)
-
- // if there are placeholders, only get up to the last complete 4 chars
- l = placeHolders > 0 ? len - 4 : len
-
- var L = 0
-
- for (i = 0, j = 0; i < l; i += 4, j += 3) {
- tmp = (revLookup[b64.charCodeAt(i)] << 18) | (revLookup[b64.charCodeAt(i + 1)] << 12) | (revLookup[b64.charCodeAt(i + 2)] << 6) | revLookup[b64.charCodeAt(i + 3)]
- arr[L++] = (tmp >> 16) & 0xFF
- arr[L++] = (tmp >> 8) & 0xFF
- arr[L++] = tmp & 0xFF
- }
-
- if (placeHolders === 2) {
- tmp = (revLookup[b64.charCodeAt(i)] << 2) | (revLookup[b64.charCodeAt(i + 1)] >> 4)
- arr[L++] = tmp & 0xFF
- } else if (placeHolders === 1) {
- tmp = (revLookup[b64.charCodeAt(i)] << 10) | (revLookup[b64.charCodeAt(i + 1)] << 4) | (revLookup[b64.charCodeAt(i + 2)] >> 2)
- arr[L++] = (tmp >> 8) & 0xFF
- arr[L++] = tmp & 0xFF
- }
-
- return arr
-}
-
-function tripletToBase64 (num) {
- return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F]
-}
-
-function encodeChunk (uint8, start, end) {
- var tmp
- var output = []
- for (var i = start; i < end; i += 3) {
- tmp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
- output.push(tripletToBase64(tmp))
- }
- return output.join('')
-}
-
-function fromByteArray (uint8) {
- var tmp
- var len = uint8.length
- var extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes
- var output = ''
- var parts = []
- var maxChunkLength = 16383 // must be multiple of 3
-
- // go through the array every three bytes, we'll deal with trailing stuff later
- for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) {
- parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength)))
- }
-
- // pad the end with zeros, but make sure to not forget the extra bytes
- if (extraBytes === 1) {
- tmp = uint8[len - 1]
- output += lookup[tmp >> 2]
- output += lookup[(tmp << 4) & 0x3F]
- output += '=='
- } else if (extraBytes === 2) {
- tmp = (uint8[len - 2] << 8) + (uint8[len - 1])
- output += lookup[tmp >> 10]
- output += lookup[(tmp >> 4) & 0x3F]
- output += lookup[(tmp << 2) & 0x3F]
- output += '='
- }
-
- parts.push(output)
-
- return parts.join('')
-}
-
-
-/***/ }),
-/* 18 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-/* WEBPACK VAR INJECTION */(function(global) {/*!
- * The buffer module from node.js, for the browser.
- *
- * @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
- * @license MIT
- */
-/* eslint-disable no-proto */
-
-
-
-var base64 = __webpack_require__(17)
-var ieee754 = __webpack_require__(23)
-var isArray = __webpack_require__(24)
-
-exports.Buffer = Buffer
-exports.SlowBuffer = SlowBuffer
-exports.INSPECT_MAX_BYTES = 50
-
-/**
- * If `Buffer.TYPED_ARRAY_SUPPORT`:
- * === true Use Uint8Array implementation (fastest)
- * === false Use Object implementation (most compatible, even IE6)
- *
- * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+,
- * Opera 11.6+, iOS 4.2+.
- *
- * Due to various browser bugs, sometimes the Object implementation will be used even
- * when the browser supports typed arrays.
- *
- * Note:
- *
- * - Firefox 4-29 lacks support for adding new properties to `Uint8Array` instances,
- * See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438.
- *
- * - Chrome 9-10 is missing the `TypedArray.prototype.subarray` function.
- *
- * - IE10 has a broken `TypedArray.prototype.subarray` function which returns arrays of
- * incorrect length in some situations.
-
- * We detect these buggy browsers and set `Buffer.TYPED_ARRAY_SUPPORT` to `false` so they
- * get the Object implementation, which is slower but behaves correctly.
- */
-Buffer.TYPED_ARRAY_SUPPORT = global.TYPED_ARRAY_SUPPORT !== undefined
- ? global.TYPED_ARRAY_SUPPORT
- : typedArraySupport()
-
-/*
- * Export kMaxLength after typed array support is determined.
- */
-exports.kMaxLength = kMaxLength()
-
-function typedArraySupport () {
- try {
- var arr = new Uint8Array(1)
- arr.__proto__ = {__proto__: Uint8Array.prototype, foo: function () { return 42 }}
- return arr.foo() === 42 && // typed array instances can be augmented
- typeof arr.subarray === 'function' && // chrome 9-10 lack `subarray`
- arr.subarray(1, 1).byteLength === 0 // ie10 has broken `subarray`
- } catch (e) {
- return false
- }
-}
-
-function kMaxLength () {
- return Buffer.TYPED_ARRAY_SUPPORT
- ? 0x7fffffff
- : 0x3fffffff
-}
-
-function createBuffer (that, length) {
- if (kMaxLength() < length) {
- throw new RangeError('Invalid typed array length')
- }
- if (Buffer.TYPED_ARRAY_SUPPORT) {
- // Return an augmented `Uint8Array` instance, for best performance
- that = new Uint8Array(length)
- that.__proto__ = Buffer.prototype
- } else {
- // Fallback: Return an object instance of the Buffer class
- if (that === null) {
- that = new Buffer(length)
- }
- that.length = length
- }
-
- return that
-}
-
-/**
- * The Buffer constructor returns instances of `Uint8Array` that have their
- * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of
- * `Uint8Array`, so the returned instances will have all the node `Buffer` methods
- * and the `Uint8Array` methods. Square bracket notation works as expected -- it
- * returns a single octet.
- *
- * The `Uint8Array` prototype remains unmodified.
- */
-
-function Buffer (arg, encodingOrOffset, length) {
- if (!Buffer.TYPED_ARRAY_SUPPORT && !(this instanceof Buffer)) {
- return new Buffer(arg, encodingOrOffset, length)
- }
-
- // Common case.
- if (typeof arg === 'number') {
- if (typeof encodingOrOffset === 'string') {
- throw new Error(
- 'If encoding is specified then the first argument must be a string'
- )
- }
- return allocUnsafe(this, arg)
- }
- return from(this, arg, encodingOrOffset, length)
-}
-
-Buffer.poolSize = 8192 // not used by this implementation
-
-// TODO: Legacy, not needed anymore. Remove in next major version.
-Buffer._augment = function (arr) {
- arr.__proto__ = Buffer.prototype
- return arr
-}
-
-function from (that, value, encodingOrOffset, length) {
- if (typeof value === 'number') {
- throw new TypeError('"value" argument must not be a number')
- }
-
- if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) {
- return fromArrayBuffer(that, value, encodingOrOffset, length)
- }
-
- if (typeof value === 'string') {
- return fromString(that, value, encodingOrOffset)
- }
-
- return fromObject(that, value)
-}
-
-/**
- * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError
- * if value is a number.
- * Buffer.from(str[, encoding])
- * Buffer.from(array)
- * Buffer.from(buffer)
- * Buffer.from(arrayBuffer[, byteOffset[, length]])
- **/
-Buffer.from = function (value, encodingOrOffset, length) {
- return from(null, value, encodingOrOffset, length)
-}
-
-if (Buffer.TYPED_ARRAY_SUPPORT) {
- Buffer.prototype.__proto__ = Uint8Array.prototype
- Buffer.__proto__ = Uint8Array
- if (typeof Symbol !== 'undefined' && Symbol.species &&
- Buffer[Symbol.species] === Buffer) {
- // Fix subarray() in ES2016. See: https://github.com/feross/buffer/pull/97
- Object.defineProperty(Buffer, Symbol.species, {
- value: null,
- configurable: true
- })
- }
-}
-
-function assertSize (size) {
- if (typeof size !== 'number') {
- throw new TypeError('"size" argument must be a number')
- } else if (size < 0) {
- throw new RangeError('"size" argument must not be negative')
- }
-}
-
-function alloc (that, size, fill, encoding) {
- assertSize(size)
- if (size <= 0) {
- return createBuffer(that, size)
- }
- if (fill !== undefined) {
- // Only pay attention to encoding if it's a string. This
- // prevents accidentally sending in a number that would
- // be interpretted as a start offset.
- return typeof encoding === 'string'
- ? createBuffer(that, size).fill(fill, encoding)
- : createBuffer(that, size).fill(fill)
- }
- return createBuffer(that, size)
-}
-
-/**
- * Creates a new filled Buffer instance.
- * alloc(size[, fill[, encoding]])
- **/
-Buffer.alloc = function (size, fill, encoding) {
- return alloc(null, size, fill, encoding)
-}
-
-function allocUnsafe (that, size) {
- assertSize(size)
- that = createBuffer(that, size < 0 ? 0 : checked(size) | 0)
- if (!Buffer.TYPED_ARRAY_SUPPORT) {
- for (var i = 0; i < size; ++i) {
- that[i] = 0
- }
- }
- return that
-}
-
-/**
- * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance.
- * */
-Buffer.allocUnsafe = function (size) {
- return allocUnsafe(null, size)
-}
-/**
- * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance.
- */
-Buffer.allocUnsafeSlow = function (size) {
- return allocUnsafe(null, size)
-}
-
-function fromString (that, string, encoding) {
- if (typeof encoding !== 'string' || encoding === '') {
- encoding = 'utf8'
- }
-
- if (!Buffer.isEncoding(encoding)) {
- throw new TypeError('"encoding" must be a valid string encoding')
- }
-
- var length = byteLength(string, encoding) | 0
- that = createBuffer(that, length)
-
- var actual = that.write(string, encoding)
-
- if (actual !== length) {
- // Writing a hex string, for example, that contains invalid characters will
- // cause everything after the first invalid character to be ignored. (e.g.
- // 'abxxcd' will be treated as 'ab')
- that = that.slice(0, actual)
- }
-
- return that
-}
-
-function fromArrayLike (that, array) {
- var length = array.length < 0 ? 0 : checked(array.length) | 0
- that = createBuffer(that, length)
- for (var i = 0; i < length; i += 1) {
- that[i] = array[i] & 255
- }
- return that
-}
-
-function fromArrayBuffer (that, array, byteOffset, length) {
- array.byteLength // this throws if `array` is not a valid ArrayBuffer
-
- if (byteOffset < 0 || array.byteLength < byteOffset) {
- throw new RangeError('\'offset\' is out of bounds')
- }
-
- if (array.byteLength < byteOffset + (length || 0)) {
- throw new RangeError('\'length\' is out of bounds')
- }
-
- if (byteOffset === undefined && length === undefined) {
- array = new Uint8Array(array)
- } else if (length === undefined) {
- array = new Uint8Array(array, byteOffset)
- } else {
- array = new Uint8Array(array, byteOffset, length)
- }
-
- if (Buffer.TYPED_ARRAY_SUPPORT) {
- // Return an augmented `Uint8Array` instance, for best performance
- that = array
- that.__proto__ = Buffer.prototype
- } else {
- // Fallback: Return an object instance of the Buffer class
- that = fromArrayLike(that, array)
- }
- return that
-}
-
-function fromObject (that, obj) {
- if (Buffer.isBuffer(obj)) {
- var len = checked(obj.length) | 0
- that = createBuffer(that, len)
-
- if (that.length === 0) {
- return that
- }
-
- obj.copy(that, 0, 0, len)
- return that
- }
-
- if (obj) {
- if ((typeof ArrayBuffer !== 'undefined' &&
- obj.buffer instanceof ArrayBuffer) || 'length' in obj) {
- if (typeof obj.length !== 'number' || isnan(obj.length)) {
- return createBuffer(that, 0)
- }
- return fromArrayLike(that, obj)
- }
-
- if (obj.type === 'Buffer' && isArray(obj.data)) {
- return fromArrayLike(that, obj.data)
- }
- }
-
- throw new TypeError('First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.')
-}
-
-function checked (length) {
- // Note: cannot use `length < kMaxLength()` here because that fails when
- // length is NaN (which is otherwise coerced to zero.)
- if (length >= kMaxLength()) {
- throw new RangeError('Attempt to allocate Buffer larger than maximum ' +
- 'size: 0x' + kMaxLength().toString(16) + ' bytes')
- }
- return length | 0
-}
-
-function SlowBuffer (length) {
- if (+length != length) { // eslint-disable-line eqeqeq
- length = 0
- }
- return Buffer.alloc(+length)
-}
-
-Buffer.isBuffer = function isBuffer (b) {
- return !!(b != null && b._isBuffer)
-}
-
-Buffer.compare = function compare (a, b) {
- if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) {
- throw new TypeError('Arguments must be Buffers')
- }
-
- if (a === b) return 0
-
- var x = a.length
- var y = b.length
-
- for (var i = 0, len = Math.min(x, y); i < len; ++i) {
- if (a[i] !== b[i]) {
- x = a[i]
- y = b[i]
- break
- }
- }
-
- if (x < y) return -1
- if (y < x) return 1
- return 0
-}
-
-Buffer.isEncoding = function isEncoding (encoding) {
- switch (String(encoding).toLowerCase()) {
- case 'hex':
- case 'utf8':
- case 'utf-8':
- case 'ascii':
- case 'latin1':
- case 'binary':
- case 'base64':
- case 'ucs2':
- case 'ucs-2':
- case 'utf16le':
- case 'utf-16le':
- return true
- default:
- return false
- }
-}
-
-Buffer.concat = function concat (list, length) {
- if (!isArray(list)) {
- throw new TypeError('"list" argument must be an Array of Buffers')
- }
-
- if (list.length === 0) {
- return Buffer.alloc(0)
- }
-
- var i
- if (length === undefined) {
- length = 0
- for (i = 0; i < list.length; ++i) {
- length += list[i].length
- }
- }
-
- var buffer = Buffer.allocUnsafe(length)
- var pos = 0
- for (i = 0; i < list.length; ++i) {
- var buf = list[i]
- if (!Buffer.isBuffer(buf)) {
- throw new TypeError('"list" argument must be an Array of Buffers')
- }
- buf.copy(buffer, pos)
- pos += buf.length
- }
- return buffer
-}
-
-function byteLength (string, encoding) {
- if (Buffer.isBuffer(string)) {
- return string.length
- }
- if (typeof ArrayBuffer !== 'undefined' && typeof ArrayBuffer.isView === 'function' &&
- (ArrayBuffer.isView(string) || string instanceof ArrayBuffer)) {
- return string.byteLength
- }
- if (typeof string !== 'string') {
- string = '' + string
- }
-
- var len = string.length
- if (len === 0) return 0
-
- // Use a for loop to avoid recursion
- var loweredCase = false
- for (;;) {
- switch (encoding) {
- case 'ascii':
- case 'latin1':
- case 'binary':
- return len
- case 'utf8':
- case 'utf-8':
- case undefined:
- return utf8ToBytes(string).length
- case 'ucs2':
- case 'ucs-2':
- case 'utf16le':
- case 'utf-16le':
- return len * 2
- case 'hex':
- return len >>> 1
- case 'base64':
- return base64ToBytes(string).length
- default:
- if (loweredCase) return utf8ToBytes(string).length // assume utf8
- encoding = ('' + encoding).toLowerCase()
- loweredCase = true
- }
- }
-}
-Buffer.byteLength = byteLength
-
-function slowToString (encoding, start, end) {
- var loweredCase = false
-
- // No need to verify that "this.length <= MAX_UINT32" since it's a read-only
- // property of a typed array.
-
- // This behaves neither like String nor Uint8Array in that we set start/end
- // to their upper/lower bounds if the value passed is out of range.
- // undefined is handled specially as per ECMA-262 6th Edition,
- // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization.
- if (start === undefined || start < 0) {
- start = 0
- }
- // Return early if start > this.length. Done here to prevent potential uint32
- // coercion fail below.
- if (start > this.length) {
- return ''
- }
-
- if (end === undefined || end > this.length) {
- end = this.length
- }
-
- if (end <= 0) {
- return ''
- }
-
- // Force coersion to uint32. This will also coerce falsey/NaN values to 0.
- end >>>= 0
- start >>>= 0
-
- if (end <= start) {
- return ''
- }
-
- if (!encoding) encoding = 'utf8'
-
- while (true) {
- switch (encoding) {
- case 'hex':
- return hexSlice(this, start, end)
-
- case 'utf8':
- case 'utf-8':
- return utf8Slice(this, start, end)
-
- case 'ascii':
- return asciiSlice(this, start, end)
-
- case 'latin1':
- case 'binary':
- return latin1Slice(this, start, end)
-
- case 'base64':
- return base64Slice(this, start, end)
-
- case 'ucs2':
- case 'ucs-2':
- case 'utf16le':
- case 'utf-16le':
- return utf16leSlice(this, start, end)
-
- default:
- if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)
- encoding = (encoding + '').toLowerCase()
- loweredCase = true
- }
- }
-}
-
-// The property is used by `Buffer.isBuffer` and `is-buffer` (in Safari 5-7) to detect
-// Buffer instances.
-Buffer.prototype._isBuffer = true
-
-function swap (b, n, m) {
- var i = b[n]
- b[n] = b[m]
- b[m] = i
-}
-
-Buffer.prototype.swap16 = function swap16 () {
- var len = this.length
- if (len % 2 !== 0) {
- throw new RangeError('Buffer size must be a multiple of 16-bits')
- }
- for (var i = 0; i < len; i += 2) {
- swap(this, i, i + 1)
- }
- return this
-}
-
-Buffer.prototype.swap32 = function swap32 () {
- var len = this.length
- if (len % 4 !== 0) {
- throw new RangeError('Buffer size must be a multiple of 32-bits')
- }
- for (var i = 0; i < len; i += 4) {
- swap(this, i, i + 3)
- swap(this, i + 1, i + 2)
- }
- return this
-}
-
-Buffer.prototype.swap64 = function swap64 () {
- var len = this.length
- if (len % 8 !== 0) {
- throw new RangeError('Buffer size must be a multiple of 64-bits')
- }
- for (var i = 0; i < len; i += 8) {
- swap(this, i, i + 7)
- swap(this, i + 1, i + 6)
- swap(this, i + 2, i + 5)
- swap(this, i + 3, i + 4)
- }
- return this
-}
-
-Buffer.prototype.toString = function toString () {
- var length = this.length | 0
- if (length === 0) return ''
- if (arguments.length === 0) return utf8Slice(this, 0, length)
- return slowToString.apply(this, arguments)
-}
-
-Buffer.prototype.equals = function equals (b) {
- if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer')
- if (this === b) return true
- return Buffer.compare(this, b) === 0
-}
-
-Buffer.prototype.inspect = function inspect () {
- var str = ''
- var max = exports.INSPECT_MAX_BYTES
- if (this.length > 0) {
- str = this.toString('hex', 0, max).match(/.{2}/g).join(' ')
- if (this.length > max) str += ' ... '
- }
- return '<Buffer ' + str + '>'
-}
-
-Buffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) {
- if (!Buffer.isBuffer(target)) {
- throw new TypeError('Argument must be a Buffer')
- }
-
- if (start === undefined) {
- start = 0
- }
- if (end === undefined) {
- end = target ? target.length : 0
- }
- if (thisStart === undefined) {
- thisStart = 0
- }
- if (thisEnd === undefined) {
- thisEnd = this.length
- }
-
- if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) {
- throw new RangeError('out of range index')
- }
-
- if (thisStart >= thisEnd && start >= end) {
- return 0
- }
- if (thisStart >= thisEnd) {
- return -1
- }
- if (start >= end) {
- return 1
- }
-
- start >>>= 0
- end >>>= 0
- thisStart >>>= 0
- thisEnd >>>= 0
-
- if (this === target) return 0
-
- var x = thisEnd - thisStart
- var y = end - start
- var len = Math.min(x, y)
-
- var thisCopy = this.slice(thisStart, thisEnd)
- var targetCopy = target.slice(start, end)
-
- for (var i = 0; i < len; ++i) {
- if (thisCopy[i] !== targetCopy[i]) {
- x = thisCopy[i]
- y = targetCopy[i]
- break
- }
- }
-
- if (x < y) return -1
- if (y < x) return 1
- return 0
-}
-
-// Finds either the first index of `val` in `buffer` at offset >= `byteOffset`,
-// OR the last index of `val` in `buffer` at offset <= `byteOffset`.
-//
-// Arguments:
-// - buffer - a Buffer to search
-// - val - a string, Buffer, or number
-// - byteOffset - an index into `buffer`; will be clamped to an int32
-// - encoding - an optional encoding, relevant is val is a string
-// - dir - true for indexOf, false for lastIndexOf
-function bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) {
- // Empty buffer means no match
- if (buffer.length === 0) return -1
-
- // Normalize byteOffset
- if (typeof byteOffset === 'string') {
- encoding = byteOffset
- byteOffset = 0
- } else if (byteOffset > 0x7fffffff) {
- byteOffset = 0x7fffffff
- } else if (byteOffset < -0x80000000) {
- byteOffset = -0x80000000
- }
- byteOffset = +byteOffset // Coerce to Number.
- if (isNaN(byteOffset)) {
- // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer
- byteOffset = dir ? 0 : (buffer.length - 1)
- }
-
- // Normalize byteOffset: negative offsets start from the end of the buffer
- if (byteOffset < 0) byteOffset = buffer.length + byteOffset
- if (byteOffset >= buffer.length) {
- if (dir) return -1
- else byteOffset = buffer.length - 1
- } else if (byteOffset < 0) {
- if (dir) byteOffset = 0
- else return -1
- }
-
- // Normalize val
- if (typeof val === 'string') {
- val = Buffer.from(val, encoding)
- }
-
- // Finally, search either indexOf (if dir is true) or lastIndexOf
- if (Buffer.isBuffer(val)) {
- // Special case: looking for empty string/buffer always fails
- if (val.length === 0) {
- return -1
- }
- return arrayIndexOf(buffer, val, byteOffset, encoding, dir)
- } else if (typeof val === 'number') {
- val = val & 0xFF // Search for a byte value [0-255]
- if (Buffer.TYPED_ARRAY_SUPPORT &&
- typeof Uint8Array.prototype.indexOf === 'function') {
- if (dir) {
- return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset)
- } else {
- return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset)
- }
- }
- return arrayIndexOf(buffer, [ val ], byteOffset, encoding, dir)
- }
-
- throw new TypeError('val must be string, number or Buffer')
-}
-
-function arrayIndexOf (arr, val, byteOffset, encoding, dir) {
- var indexSize = 1
- var arrLength = arr.length
- var valLength = val.length
-
- if (encoding !== undefined) {
- encoding = String(encoding).toLowerCase()
- if (encoding === 'ucs2' || encoding === 'ucs-2' ||
- encoding === 'utf16le' || encoding === 'utf-16le') {
- if (arr.length < 2 || val.length < 2) {
- return -1
- }
- indexSize = 2
- arrLength /= 2
- valLength /= 2
- byteOffset /= 2
- }
- }
-
- function read (buf, i) {
- if (indexSize === 1) {
- return buf[i]
- } else {
- return buf.readUInt16BE(i * indexSize)
- }
- }
-
- var i
- if (dir) {
- var foundIndex = -1
- for (i = byteOffset; i < arrLength; i++) {
- if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) {
- if (foundIndex === -1) foundIndex = i
- if (i - foundIndex + 1 === valLength) return foundIndex * indexSize
- } else {
- if (foundIndex !== -1) i -= i - foundIndex
- foundIndex = -1
- }
- }
- } else {
- if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength
- for (i = byteOffset; i >= 0; i--) {
- var found = true
- for (var j = 0; j < valLength; j++) {
- if (read(arr, i + j) !== read(val, j)) {
- found = false
- break
- }
- }
- if (found) return i
- }
- }
-
- return -1
-}
-
-Buffer.prototype.includes = function includes (val, byteOffset, encoding) {
- return this.indexOf(val, byteOffset, encoding) !== -1
-}
-
-Buffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) {
- return bidirectionalIndexOf(this, val, byteOffset, encoding, true)
-}
-
-Buffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) {
- return bidirectionalIndexOf(this, val, byteOffset, encoding, false)
-}
-
-function hexWrite (buf, string, offset, length) {
- offset = Number(offset) || 0
- var remaining = buf.length - offset
- if (!length) {
- length = remaining
- } else {
- length = Number(length)
- if (length > remaining) {
- length = remaining
- }
- }
-
- // must be an even number of digits
- var strLen = string.length
- if (strLen % 2 !== 0) throw new TypeError('Invalid hex string')
-
- if (length > strLen / 2) {
- length = strLen / 2
- }
- for (var i = 0; i < length; ++i) {
- var parsed = parseInt(string.substr(i * 2, 2), 16)
- if (isNaN(parsed)) return i
- buf[offset + i] = parsed
- }
- return i
-}
-
-function utf8Write (buf, string, offset, length) {
- return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length)
-}
-
-function asciiWrite (buf, string, offset, length) {
- return blitBuffer(asciiToBytes(string), buf, offset, length)
-}
-
-function latin1Write (buf, string, offset, length) {
- return asciiWrite(buf, string, offset, length)
-}
-
-function base64Write (buf, string, offset, length) {
- return blitBuffer(base64ToBytes(string), buf, offset, length)
-}
-
-function ucs2Write (buf, string, offset, length) {
- return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length)
-}
-
-Buffer.prototype.write = function write (string, offset, length, encoding) {
- // Buffer#write(string)
- if (offset === undefined) {
- encoding = 'utf8'
- length = this.length
- offset = 0
- // Buffer#write(string, encoding)
- } else if (length === undefined && typeof offset === 'string') {
- encoding = offset
- length = this.length
- offset = 0
- // Buffer#write(string, offset[, length][, encoding])
- } else if (isFinite(offset)) {
- offset = offset | 0
- if (isFinite(length)) {
- length = length | 0
- if (encoding === undefined) encoding = 'utf8'
- } else {
- encoding = length
- length = undefined
- }
- // legacy write(string, encoding, offset, length) - remove in v0.13
- } else {
- throw new Error(
- 'Buffer.write(string, encoding, offset[, length]) is no longer supported'
- )
- }
-
- var remaining = this.length - offset
- if (length === undefined || length > remaining) length = remaining
-
- if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) {
- throw new RangeError('Attempt to write outside buffer bounds')
- }
-
- if (!encoding) encoding = 'utf8'
-
- var loweredCase = false
- for (;;) {
- switch (encoding) {
- case 'hex':
- return hexWrite(this, string, offset, length)
-
- case 'utf8':
- case 'utf-8':
- return utf8Write(this, string, offset, length)
-
- case 'ascii':
- return asciiWrite(this, string, offset, length)
-
- case 'latin1':
- case 'binary':
- return latin1Write(this, string, offset, length)
-
- case 'base64':
- // Warning: maxLength not taken into account in base64Write
- return base64Write(this, string, offset, length)
-
- case 'ucs2':
- case 'ucs-2':
- case 'utf16le':
- case 'utf-16le':
- return ucs2Write(this, string, offset, length)
-
- default:
- if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)
- encoding = ('' + encoding).toLowerCase()
- loweredCase = true
- }
- }
-}
-
-Buffer.prototype.toJSON = function toJSON () {
- return {
- type: 'Buffer',
- data: Array.prototype.slice.call(this._arr || this, 0)
- }
-}
-
-function base64Slice (buf, start, end) {
- if (start === 0 && end === buf.length) {
- return base64.fromByteArray(buf)
- } else {
- return base64.fromByteArray(buf.slice(start, end))
- }
-}
-
-function utf8Slice (buf, start, end) {
- end = Math.min(buf.length, end)
- var res = []
-
- var i = start
- while (i < end) {
- var firstByte = buf[i]
- var codePoint = null
- var bytesPerSequence = (firstByte > 0xEF) ? 4
- : (firstByte > 0xDF) ? 3
- : (firstByte > 0xBF) ? 2
- : 1
-
- if (i + bytesPerSequence <= end) {
- var secondByte, thirdByte, fourthByte, tempCodePoint
-
- switch (bytesPerSequence) {
- case 1:
- if (firstByte < 0x80) {
- codePoint = firstByte
- }
- break
- case 2:
- secondByte = buf[i + 1]
- if ((secondByte & 0xC0) === 0x80) {
- tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F)
- if (tempCodePoint > 0x7F) {
- codePoint = tempCodePoint
- }
- }
- break
- case 3:
- secondByte = buf[i + 1]
- thirdByte = buf[i + 2]
- if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) {
- tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F)
- if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) {
- codePoint = tempCodePoint
- }
- }
- break
- case 4:
- secondByte = buf[i + 1]
- thirdByte = buf[i + 2]
- fourthByte = buf[i + 3]
- if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) {
- tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F)
- if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) {
- codePoint = tempCodePoint
- }
- }
- }
- }
-
- if (codePoint === null) {
- // we did not generate a valid codePoint so insert a
- // replacement char (U+FFFD) and advance only 1 byte
- codePoint = 0xFFFD
- bytesPerSequence = 1
- } else if (codePoint > 0xFFFF) {
- // encode to utf16 (surrogate pair dance)
- codePoint -= 0x10000
- res.push(codePoint >>> 10 & 0x3FF | 0xD800)
- codePoint = 0xDC00 | codePoint & 0x3FF
- }
-
- res.push(codePoint)
- i += bytesPerSequence
- }
-
- return decodeCodePointsArray(res)
-}
-
-// Based on http://stackoverflow.com/a/22747272/680742, the browser with
-// the lowest limit is Chrome, with 0x10000 args.
-// We go 1 magnitude less, for safety
-var MAX_ARGUMENTS_LENGTH = 0x1000
-
-function decodeCodePointsArray (codePoints) {
- var len = codePoints.length
- if (len <= MAX_ARGUMENTS_LENGTH) {
- return String.fromCharCode.apply(String, codePoints) // avoid extra slice()
- }
-
- // Decode in chunks to avoid "call stack size exceeded".
- var res = ''
- var i = 0
- while (i < len) {
- res += String.fromCharCode.apply(
- String,
- codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH)
- )
- }
- return res
-}
-
-function asciiSlice (buf, start, end) {
- var ret = ''
- end = Math.min(buf.length, end)
-
- for (var i = start; i < end; ++i) {
- ret += String.fromCharCode(buf[i] & 0x7F)
- }
- return ret
-}
-
-function latin1Slice (buf, start, end) {
- var ret = ''
- end = Math.min(buf.length, end)
-
- for (var i = start; i < end; ++i) {
- ret += String.fromCharCode(buf[i])
- }
- return ret
-}
-
-function hexSlice (buf, start, end) {
- var len = buf.length
-
- if (!start || start < 0) start = 0
- if (!end || end < 0 || end > len) end = len
-
- var out = ''
- for (var i = start; i < end; ++i) {
- out += toHex(buf[i])
- }
- return out
-}
-
-function utf16leSlice (buf, start, end) {
- var bytes = buf.slice(start, end)
- var res = ''
- for (var i = 0; i < bytes.length; i += 2) {
- res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256)
- }
- return res
-}
-
-Buffer.prototype.slice = function slice (start, end) {
- var len = this.length
- start = ~~start
- end = end === undefined ? len : ~~end
-
- if (start < 0) {
- start += len
- if (start < 0) start = 0
- } else if (start > len) {
- start = len
- }
-
- if (end < 0) {
- end += len
- if (end < 0) end = 0
- } else if (end > len) {
- end = len
- }
-
- if (end < start) end = start
-
- var newBuf
- if (Buffer.TYPED_ARRAY_SUPPORT) {
- newBuf = this.subarray(start, end)
- newBuf.__proto__ = Buffer.prototype
- } else {
- var sliceLen = end - start
- newBuf = new Buffer(sliceLen, undefined)
- for (var i = 0; i < sliceLen; ++i) {
- newBuf[i] = this[i + start]
- }
- }
-
- return newBuf
-}
-
-/*
- * Need to make sure that buffer isn't trying to write out of bounds.
- */
-function checkOffset (offset, ext, length) {
- if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint')
- if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length')
-}
-
-Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) {
- offset = offset | 0
- byteLength = byteLength | 0
- if (!noAssert) checkOffset(offset, byteLength, this.length)
-
- var val = this[offset]
- var mul = 1
- var i = 0
- while (++i < byteLength && (mul *= 0x100)) {
- val += this[offset + i] * mul
- }
-
- return val
-}
-
-Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) {
- offset = offset | 0
- byteLength = byteLength | 0
- if (!noAssert) {
- checkOffset(offset, byteLength, this.length)
- }
-
- var val = this[offset + --byteLength]
- var mul = 1
- while (byteLength > 0 && (mul *= 0x100)) {
- val += this[offset + --byteLength] * mul
- }
-
- return val
-}
-
-Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 1, this.length)
- return this[offset]
-}
-
-Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 2, this.length)
- return this[offset] | (this[offset + 1] << 8)
-}
-
-Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 2, this.length)
- return (this[offset] << 8) | this[offset + 1]
-}
-
-Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 4, this.length)
-
- return ((this[offset]) |
- (this[offset + 1] << 8) |
- (this[offset + 2] << 16)) +
- (this[offset + 3] * 0x1000000)
-}
-
-Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 4, this.length)
-
- return (this[offset] * 0x1000000) +
- ((this[offset + 1] << 16) |
- (this[offset + 2] << 8) |
- this[offset + 3])
-}
-
-Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) {
- offset = offset | 0
- byteLength = byteLength | 0
- if (!noAssert) checkOffset(offset, byteLength, this.length)
-
- var val = this[offset]
- var mul = 1
- var i = 0
- while (++i < byteLength && (mul *= 0x100)) {
- val += this[offset + i] * mul
- }
- mul *= 0x80
-
- if (val >= mul) val -= Math.pow(2, 8 * byteLength)
-
- return val
-}
-
-Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) {
- offset = offset | 0
- byteLength = byteLength | 0
- if (!noAssert) checkOffset(offset, byteLength, this.length)
-
- var i = byteLength
- var mul = 1
- var val = this[offset + --i]
- while (i > 0 && (mul *= 0x100)) {
- val += this[offset + --i] * mul
- }
- mul *= 0x80
-
- if (val >= mul) val -= Math.pow(2, 8 * byteLength)
-
- return val
-}
-
-Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 1, this.length)
- if (!(this[offset] & 0x80)) return (this[offset])
- return ((0xff - this[offset] + 1) * -1)
-}
-
-Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 2, this.length)
- var val = this[offset] | (this[offset + 1] << 8)
- return (val & 0x8000) ? val | 0xFFFF0000 : val
-}
-
-Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 2, this.length)
- var val = this[offset + 1] | (this[offset] << 8)
- return (val & 0x8000) ? val | 0xFFFF0000 : val
-}
-
-Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 4, this.length)
-
- return (this[offset]) |
- (this[offset + 1] << 8) |
- (this[offset + 2] << 16) |
- (this[offset + 3] << 24)
-}
-
-Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 4, this.length)
-
- return (this[offset] << 24) |
- (this[offset + 1] << 16) |
- (this[offset + 2] << 8) |
- (this[offset + 3])
-}
-
-Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 4, this.length)
- return ieee754.read(this, offset, true, 23, 4)
-}
-
-Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 4, this.length)
- return ieee754.read(this, offset, false, 23, 4)
-}
-
-Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 8, this.length)
- return ieee754.read(this, offset, true, 52, 8)
-}
-
-Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) {
- if (!noAssert) checkOffset(offset, 8, this.length)
- return ieee754.read(this, offset, false, 52, 8)
-}
-
-function checkInt (buf, value, offset, ext, max, min) {
- if (!Buffer.isBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance')
- if (value > max || value < min) throw new RangeError('"value" argument is out of bounds')
- if (offset + ext > buf.length) throw new RangeError('Index out of range')
-}
-
-Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) {
- value = +value
- offset = offset | 0
- byteLength = byteLength | 0
- if (!noAssert) {
- var maxBytes = Math.pow(2, 8 * byteLength) - 1
- checkInt(this, value, offset, byteLength, maxBytes, 0)
- }
-
- var mul = 1
- var i = 0
- this[offset] = value & 0xFF
- while (++i < byteLength && (mul *= 0x100)) {
- this[offset + i] = (value / mul) & 0xFF
- }
-
- return offset + byteLength
-}
-
-Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) {
- value = +value
- offset = offset | 0
- byteLength = byteLength | 0
- if (!noAssert) {
- var maxBytes = Math.pow(2, 8 * byteLength) - 1
- checkInt(this, value, offset, byteLength, maxBytes, 0)
- }
-
- var i = byteLength - 1
- var mul = 1
- this[offset + i] = value & 0xFF
- while (--i >= 0 && (mul *= 0x100)) {
- this[offset + i] = (value / mul) & 0xFF
- }
-
- return offset + byteLength
-}
-
-Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0)
- if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value)
- this[offset] = (value & 0xff)
- return offset + 1
-}
-
-function objectWriteUInt16 (buf, value, offset, littleEndian) {
- if (value < 0) value = 0xffff + value + 1
- for (var i = 0, j = Math.min(buf.length - offset, 2); i < j; ++i) {
- buf[offset + i] = (value & (0xff << (8 * (littleEndian ? i : 1 - i)))) >>>
- (littleEndian ? i : 1 - i) * 8
- }
-}
-
-Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0)
- if (Buffer.TYPED_ARRAY_SUPPORT) {
- this[offset] = (value & 0xff)
- this[offset + 1] = (value >>> 8)
- } else {
- objectWriteUInt16(this, value, offset, true)
- }
- return offset + 2
-}
-
-Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0)
- if (Buffer.TYPED_ARRAY_SUPPORT) {
- this[offset] = (value >>> 8)
- this[offset + 1] = (value & 0xff)
- } else {
- objectWriteUInt16(this, value, offset, false)
- }
- return offset + 2
-}
-
-function objectWriteUInt32 (buf, value, offset, littleEndian) {
- if (value < 0) value = 0xffffffff + value + 1
- for (var i = 0, j = Math.min(buf.length - offset, 4); i < j; ++i) {
- buf[offset + i] = (value >>> (littleEndian ? i : 3 - i) * 8) & 0xff
- }
-}
-
-Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0)
- if (Buffer.TYPED_ARRAY_SUPPORT) {
- this[offset + 3] = (value >>> 24)
- this[offset + 2] = (value >>> 16)
- this[offset + 1] = (value >>> 8)
- this[offset] = (value & 0xff)
- } else {
- objectWriteUInt32(this, value, offset, true)
- }
- return offset + 4
-}
-
-Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0)
- if (Buffer.TYPED_ARRAY_SUPPORT) {
- this[offset] = (value >>> 24)
- this[offset + 1] = (value >>> 16)
- this[offset + 2] = (value >>> 8)
- this[offset + 3] = (value & 0xff)
- } else {
- objectWriteUInt32(this, value, offset, false)
- }
- return offset + 4
-}
-
-Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) {
- var limit = Math.pow(2, 8 * byteLength - 1)
-
- checkInt(this, value, offset, byteLength, limit - 1, -limit)
- }
-
- var i = 0
- var mul = 1
- var sub = 0
- this[offset] = value & 0xFF
- while (++i < byteLength && (mul *= 0x100)) {
- if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) {
- sub = 1
- }
- this[offset + i] = ((value / mul) >> 0) - sub & 0xFF
- }
-
- return offset + byteLength
-}
-
-Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) {
- var limit = Math.pow(2, 8 * byteLength - 1)
-
- checkInt(this, value, offset, byteLength, limit - 1, -limit)
- }
-
- var i = byteLength - 1
- var mul = 1
- var sub = 0
- this[offset + i] = value & 0xFF
- while (--i >= 0 && (mul *= 0x100)) {
- if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) {
- sub = 1
- }
- this[offset + i] = ((value / mul) >> 0) - sub & 0xFF
- }
-
- return offset + byteLength
-}
-
-Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80)
- if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value)
- if (value < 0) value = 0xff + value + 1
- this[offset] = (value & 0xff)
- return offset + 1
-}
-
-Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000)
- if (Buffer.TYPED_ARRAY_SUPPORT) {
- this[offset] = (value & 0xff)
- this[offset + 1] = (value >>> 8)
- } else {
- objectWriteUInt16(this, value, offset, true)
- }
- return offset + 2
-}
-
-Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000)
- if (Buffer.TYPED_ARRAY_SUPPORT) {
- this[offset] = (value >>> 8)
- this[offset + 1] = (value & 0xff)
- } else {
- objectWriteUInt16(this, value, offset, false)
- }
- return offset + 2
-}
-
-Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)
- if (Buffer.TYPED_ARRAY_SUPPORT) {
- this[offset] = (value & 0xff)
- this[offset + 1] = (value >>> 8)
- this[offset + 2] = (value >>> 16)
- this[offset + 3] = (value >>> 24)
- } else {
- objectWriteUInt32(this, value, offset, true)
- }
- return offset + 4
-}
-
-Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) {
- value = +value
- offset = offset | 0
- if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)
- if (value < 0) value = 0xffffffff + value + 1
- if (Buffer.TYPED_ARRAY_SUPPORT) {
- this[offset] = (value >>> 24)
- this[offset + 1] = (value >>> 16)
- this[offset + 2] = (value >>> 8)
- this[offset + 3] = (value & 0xff)
- } else {
- objectWriteUInt32(this, value, offset, false)
- }
- return offset + 4
-}
-
-function checkIEEE754 (buf, value, offset, ext, max, min) {
- if (offset + ext > buf.length) throw new RangeError('Index out of range')
- if (offset < 0) throw new RangeError('Index out of range')
-}
-
-function writeFloat (buf, value, offset, littleEndian, noAssert) {
- if (!noAssert) {
- checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38)
- }
- ieee754.write(buf, value, offset, littleEndian, 23, 4)
- return offset + 4
-}
-
-Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) {
- return writeFloat(this, value, offset, true, noAssert)
-}
-
-Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) {
- return writeFloat(this, value, offset, false, noAssert)
-}
-
-function writeDouble (buf, value, offset, littleEndian, noAssert) {
- if (!noAssert) {
- checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308)
- }
- ieee754.write(buf, value, offset, littleEndian, 52, 8)
- return offset + 8
-}
-
-Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) {
- return writeDouble(this, value, offset, true, noAssert)
-}
-
-Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) {
- return writeDouble(this, value, offset, false, noAssert)
-}
-
-// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length)
-Buffer.prototype.copy = function copy (target, targetStart, start, end) {
- if (!start) start = 0
- if (!end && end !== 0) end = this.length
- if (targetStart >= target.length) targetStart = target.length
- if (!targetStart) targetStart = 0
- if (end > 0 && end < start) end = start
-
- // Copy 0 bytes; we're done
- if (end === start) return 0
- if (target.length === 0 || this.length === 0) return 0
-
- // Fatal error conditions
- if (targetStart < 0) {
- throw new RangeError('targetStart out of bounds')
- }
- if (start < 0 || start >= this.length) throw new RangeError('sourceStart out of bounds')
- if (end < 0) throw new RangeError('sourceEnd out of bounds')
-
- // Are we oob?
- if (end > this.length) end = this.length
- if (target.length - targetStart < end - start) {
- end = target.length - targetStart + start
- }
-
- var len = end - start
- var i
-
- if (this === target && start < targetStart && targetStart < end) {
- // descending copy from end
- for (i = len - 1; i >= 0; --i) {
- target[i + targetStart] = this[i + start]
- }
- } else if (len < 1000 || !Buffer.TYPED_ARRAY_SUPPORT) {
- // ascending copy from start
- for (i = 0; i < len; ++i) {
- target[i + targetStart] = this[i + start]
- }
- } else {
- Uint8Array.prototype.set.call(
- target,
- this.subarray(start, start + len),
- targetStart
- )
- }
-
- return len
-}
-
-// Usage:
-// buffer.fill(number[, offset[, end]])
-// buffer.fill(buffer[, offset[, end]])
-// buffer.fill(string[, offset[, end]][, encoding])
-Buffer.prototype.fill = function fill (val, start, end, encoding) {
- // Handle string cases:
- if (typeof val === 'string') {
- if (typeof start === 'string') {
- encoding = start
- start = 0
- end = this.length
- } else if (typeof end === 'string') {
- encoding = end
- end = this.length
- }
- if (val.length === 1) {
- var code = val.charCodeAt(0)
- if (code < 256) {
- val = code
- }
- }
- if (encoding !== undefined && typeof encoding !== 'string') {
- throw new TypeError('encoding must be a string')
- }
- if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) {
- throw new TypeError('Unknown encoding: ' + encoding)
- }
- } else if (typeof val === 'number') {
- val = val & 255
- }
-
- // Invalid ranges are not set to a default, so can range check early.
- if (start < 0 || this.length < start || this.length < end) {
- throw new RangeError('Out of range index')
- }
-
- if (end <= start) {
- return this
- }
-
- start = start >>> 0
- end = end === undefined ? this.length : end >>> 0
-
- if (!val) val = 0
-
- var i
- if (typeof val === 'number') {
- for (i = start; i < end; ++i) {
- this[i] = val
- }
- } else {
- var bytes = Buffer.isBuffer(val)
- ? val
- : utf8ToBytes(new Buffer(val, encoding).toString())
- var len = bytes.length
- for (i = 0; i < end - start; ++i) {
- this[i + start] = bytes[i % len]
- }
- }
-
- return this
-}
-
-// HELPER FUNCTIONS
-// ================
-
-var INVALID_BASE64_RE = /[^+\/0-9A-Za-z-_]/g
-
-function base64clean (str) {
- // Node strips out invalid characters like \n and \t from the string, base64-js does not
- str = stringtrim(str).replace(INVALID_BASE64_RE, '')
- // Node converts strings with length < 2 to ''
- if (str.length < 2) return ''
- // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not
- while (str.length % 4 !== 0) {
- str = str + '='
- }
- return str
-}
-
-function stringtrim (str) {
- if (str.trim) return str.trim()
- return str.replace(/^\s+|\s+$/g, '')
-}
-
-function toHex (n) {
- if (n < 16) return '0' + n.toString(16)
- return n.toString(16)
-}
-
-function utf8ToBytes (string, units) {
- units = units || Infinity
- var codePoint
- var length = string.length
- var leadSurrogate = null
- var bytes = []
-
- for (var i = 0; i < length; ++i) {
- codePoint = string.charCodeAt(i)
-
- // is surrogate component
- if (codePoint > 0xD7FF && codePoint < 0xE000) {
- // last char was a lead
- if (!leadSurrogate) {
- // no lead yet
- if (codePoint > 0xDBFF) {
- // unexpected trail
- if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
- continue
- } else if (i + 1 === length) {
- // unpaired lead
- if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
- continue
- }
-
- // valid lead
- leadSurrogate = codePoint
-
- continue
- }
-
- // 2 leads in a row
- if (codePoint < 0xDC00) {
- if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
- leadSurrogate = codePoint
- continue
- }
-
- // valid surrogate pair
- codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000
- } else if (leadSurrogate) {
- // valid bmp char, but last char was a lead
- if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
- }
-
- leadSurrogate = null
-
- // encode utf8
- if (codePoint < 0x80) {
- if ((units -= 1) < 0) break
- bytes.push(codePoint)
- } else if (codePoint < 0x800) {
- if ((units -= 2) < 0) break
- bytes.push(
- codePoint >> 0x6 | 0xC0,
- codePoint & 0x3F | 0x80
- )
- } else if (codePoint < 0x10000) {
- if ((units -= 3) < 0) break
- bytes.push(
- codePoint >> 0xC | 0xE0,
- codePoint >> 0x6 & 0x3F | 0x80,
- codePoint & 0x3F | 0x80
- )
- } else if (codePoint < 0x110000) {
- if ((units -= 4) < 0) break
- bytes.push(
- codePoint >> 0x12 | 0xF0,
- codePoint >> 0xC & 0x3F | 0x80,
- codePoint >> 0x6 & 0x3F | 0x80,
- codePoint & 0x3F | 0x80
- )
- } else {
- throw new Error('Invalid code point')
- }
- }
-
- return bytes
-}
-
-function asciiToBytes (str) {
- var byteArray = []
- for (var i = 0; i < str.length; ++i) {
- // Node's code seems to be doing this and not & 0x7F..
- byteArray.push(str.charCodeAt(i) & 0xFF)
- }
- return byteArray
-}
-
-function utf16leToBytes (str, units) {
- var c, hi, lo
- var byteArray = []
- for (var i = 0; i < str.length; ++i) {
- if ((units -= 2) < 0) break
-
- c = str.charCodeAt(i)
- hi = c >> 8
- lo = c % 256
- byteArray.push(lo)
- byteArray.push(hi)
- }
-
- return byteArray
-}
-
-function base64ToBytes (str) {
- return base64.toByteArray(base64clean(str))
-}
-
-function blitBuffer (src, dst, offset, length) {
- for (var i = 0; i < length; ++i) {
- if ((i + offset >= dst.length) || (i >= src.length)) break
- dst[i + offset] = src[i]
- }
- return i
-}
-
-function isnan (val) {
- return val !== val // eslint-disable-line no-self-compare
-}
-
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
-
-/***/ }),
-/* 19 */
-/***/ (function(module, exports, __webpack_require__) {
-
-exports = module.exports = __webpack_require__(1)(undefined);
-// imports
-
-
-// module
-exports.push([module.i, ".cell[data-v-3ac4c361]{flex-direction:column}", ""]);
-
-// exports
-
-
-/***/ }),
-/* 20 */
-/***/ (function(module, exports, __webpack_require__) {
-
-exports = module.exports = __webpack_require__(1)(undefined);
-// imports
-
-
-// module
-exports.push([module.i, ".cell,.input,.output{display:flex;width:100%;margin-bottom:10px}.cell pre{margin:0;width:100%}", ""]);
-
-// exports
-
-
-/***/ }),
-/* 21 */
-/***/ (function(module, exports, __webpack_require__) {
-
-exports = module.exports = __webpack_require__(1)(undefined);
-// imports
-
-
-// module
-exports.push([module.i, ".prompt[data-v-4f6bf458]{padding:0 10px;min-width:7em;font-family:monospace}", ""]);
-
-// exports
-
-
-/***/ }),
-/* 22 */
-/***/ (function(module, exports, __webpack_require__) {
-
-exports = module.exports = __webpack_require__(1)(undefined);
-// imports
-
-
-// module
-exports.push([module.i, ".markdown .katex{display:block;text-align:center}.markdown .inline-katex .katex{display:inline;text-align:initial}", ""]);
-
-// exports
-
-
-/***/ }),
-/* 23 */
-/***/ (function(module, exports) {
-
-exports.read = function (buffer, offset, isLE, mLen, nBytes) {
- var e, m
- var eLen = nBytes * 8 - mLen - 1
- var eMax = (1 << eLen) - 1
- var eBias = eMax >> 1
- var nBits = -7
- var i = isLE ? (nBytes - 1) : 0
- var d = isLE ? -1 : 1
- var s = buffer[offset + i]
-
- i += d
-
- e = s & ((1 << (-nBits)) - 1)
- s >>= (-nBits)
- nBits += eLen
- for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8) {}
-
- m = e & ((1 << (-nBits)) - 1)
- e >>= (-nBits)
- nBits += mLen
- for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8) {}
-
- if (e === 0) {
- e = 1 - eBias
- } else if (e === eMax) {
- return m ? NaN : ((s ? -1 : 1) * Infinity)
- } else {
- m = m + Math.pow(2, mLen)
- e = e - eBias
- }
- return (s ? -1 : 1) * m * Math.pow(2, e - mLen)
-}
-
-exports.write = function (buffer, value, offset, isLE, mLen, nBytes) {
- var e, m, c
- var eLen = nBytes * 8 - mLen - 1
- var eMax = (1 << eLen) - 1
- var eBias = eMax >> 1
- var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0)
- var i = isLE ? 0 : (nBytes - 1)
- var d = isLE ? 1 : -1
- var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0
-
- value = Math.abs(value)
-
- if (isNaN(value) || value === Infinity) {
- m = isNaN(value) ? 1 : 0
- e = eMax
- } else {
- e = Math.floor(Math.log(value) / Math.LN2)
- if (value * (c = Math.pow(2, -e)) < 1) {
- e--
- c *= 2
- }
- if (e + eBias >= 1) {
- value += rt / c
- } else {
- value += rt * Math.pow(2, 1 - eBias)
- }
- if (value * c >= 2) {
- e++
- c /= 2
- }
-
- if (e + eBias >= eMax) {
- m = 0
- e = eMax
- } else if (e + eBias >= 1) {
- m = (value * c - 1) * Math.pow(2, mLen)
- e = e + eBias
- } else {
- m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen)
- e = 0
- }
- }
-
- for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {}
-
- e = (e << mLen) | m
- eLen += mLen
- for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {}
-
- buffer[offset + i - d] |= s * 128
-}
-
-
-/***/ }),
-/* 24 */
-/***/ (function(module, exports) {
-
-var toString = {}.toString;
-
-module.exports = Array.isArray || function (arr) {
- return toString.call(arr) == '[object Array]';
-};
-
-
-/***/ }),
-/* 25 */
-/***/ (function(module, exports, __webpack_require__) {
-
-/* WEBPACK VAR INJECTION */(function(global) {/**
- * marked - a markdown parser
- * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)
- * https://github.com/chjj/marked
- */
-
-;(function() {
-
-/**
- * Block-Level Grammar
- */
-
-var block = {
- newline: /^\n+/,
- code: /^( {4}[^\n]+\n*)+/,
- fences: noop,
- hr: /^( *[-*_]){3,} *(?:\n+|$)/,
- heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
- nptable: noop,
- lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,
- blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,
- list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
- html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,
- def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
- table: noop,
- paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,
- text: /^[^\n]+/
-};
-
-block.bullet = /(?:[*+-]|\d+\.)/;
-block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;
-block.item = replace(block.item, 'gm')
- (/bull/g, block.bullet)
- ();
-
-block.list = replace(block.list)
- (/bull/g, block.bullet)
- ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))')
- ('def', '\\n+(?=' + block.def.source + ')')
- ();
-
-block.blockquote = replace(block.blockquote)
- ('def', block.def)
- ();
-
-block._tag = '(?!(?:'
- + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code'
- + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo'
- + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b';
-
-block.html = replace(block.html)
- ('comment', /<!--[\s\S]*?-->/)
- ('closed', /<(tag)[\s\S]+?<\/\1>/)
- ('closing', /<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/)
- (/tag/g, block._tag)
- ();
-
-block.paragraph = replace(block.paragraph)
- ('hr', block.hr)
- ('heading', block.heading)
- ('lheading', block.lheading)
- ('blockquote', block.blockquote)
- ('tag', '<' + block._tag)
- ('def', block.def)
- ();
-
-/**
- * Normal Block Grammar
- */
-
-block.normal = merge({}, block);
-
-/**
- * GFM Block Grammar
- */
-
-block.gfm = merge({}, block.normal, {
- fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,
- paragraph: /^/,
- heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/
-});
-
-block.gfm.paragraph = replace(block.paragraph)
- ('(?!', '(?!'
- + block.gfm.fences.source.replace('\\1', '\\2') + '|'
- + block.list.source.replace('\\1', '\\3') + '|')
- ();
-
-/**
- * GFM + Tables Block Grammar
- */
-
-block.tables = merge({}, block.gfm, {
- nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,
- table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/
-});
-
-/**
- * Block Lexer
- */
-
-function Lexer(options) {
- this.tokens = [];
- this.tokens.links = {};
- this.options = options || marked.defaults;
- this.rules = block.normal;
-
- if (this.options.gfm) {
- if (this.options.tables) {
- this.rules = block.tables;
- } else {
- this.rules = block.gfm;
- }
- }
-}
-
-/**
- * Expose Block Rules
- */
-
-Lexer.rules = block;
-
-/**
- * Static Lex Method
- */
-
-Lexer.lex = function(src, options) {
- var lexer = new Lexer(options);
- return lexer.lex(src);
-};
-
-/**
- * Preprocessing
- */
-
-Lexer.prototype.lex = function(src) {
- src = src
- .replace(/\r\n|\r/g, '\n')
- .replace(/\t/g, ' ')
- .replace(/\u00a0/g, ' ')
- .replace(/\u2424/g, '\n');
-
- return this.token(src, true);
-};
-
-/**
- * Lexing
- */
-
-Lexer.prototype.token = function(src, top, bq) {
- var src = src.replace(/^ +$/gm, '')
- , next
- , loose
- , cap
- , bull
- , b
- , item
- , space
- , i
- , l;
-
- while (src) {
- // newline
- if (cap = this.rules.newline.exec(src)) {
- src = src.substring(cap[0].length);
- if (cap[0].length > 1) {
- this.tokens.push({
- type: 'space'
- });
- }
- }
-
- // code
- if (cap = this.rules.code.exec(src)) {
- src = src.substring(cap[0].length);
- cap = cap[0].replace(/^ {4}/gm, '');
- this.tokens.push({
- type: 'code',
- text: !this.options.pedantic
- ? cap.replace(/\n+$/, '')
- : cap
- });
- continue;
- }
-
- // fences (gfm)
- if (cap = this.rules.fences.exec(src)) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'code',
- lang: cap[2],
- text: cap[3] || ''
- });
- continue;
- }
-
- // heading
- if (cap = this.rules.heading.exec(src)) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'heading',
- depth: cap[1].length,
- text: cap[2]
- });
- continue;
- }
-
- // table no leading pipe (gfm)
- if (top && (cap = this.rules.nptable.exec(src))) {
- src = src.substring(cap[0].length);
-
- item = {
- type: 'table',
- header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
- align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
- cells: cap[3].replace(/\n$/, '').split('\n')
- };
-
- for (i = 0; i < item.align.length; i++) {
- if (/^ *-+: *$/.test(item.align[i])) {
- item.align[i] = 'right';
- } else if (/^ *:-+: *$/.test(item.align[i])) {
- item.align[i] = 'center';
- } else if (/^ *:-+ *$/.test(item.align[i])) {
- item.align[i] = 'left';
- } else {
- item.align[i] = null;
- }
- }
-
- for (i = 0; i < item.cells.length; i++) {
- item.cells[i] = item.cells[i].split(/ *\| */);
- }
-
- this.tokens.push(item);
-
- continue;
- }
-
- // lheading
- if (cap = this.rules.lheading.exec(src)) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'heading',
- depth: cap[2] === '=' ? 1 : 2,
- text: cap[1]
- });
- continue;
- }
-
- // hr
- if (cap = this.rules.hr.exec(src)) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'hr'
- });
- continue;
- }
-
- // blockquote
- if (cap = this.rules.blockquote.exec(src)) {
- src = src.substring(cap[0].length);
-
- this.tokens.push({
- type: 'blockquote_start'
- });
-
- cap = cap[0].replace(/^ *> ?/gm, '');
-
- // Pass `top` to keep the current
- // "toplevel" state. This is exactly
- // how markdown.pl works.
- this.token(cap, top, true);
-
- this.tokens.push({
- type: 'blockquote_end'
- });
-
- continue;
- }
-
- // list
- if (cap = this.rules.list.exec(src)) {
- src = src.substring(cap[0].length);
- bull = cap[2];
-
- this.tokens.push({
- type: 'list_start',
- ordered: bull.length > 1
- });
-
- // Get each top-level item.
- cap = cap[0].match(this.rules.item);
-
- next = false;
- l = cap.length;
- i = 0;
-
- for (; i < l; i++) {
- item = cap[i];
-
- // Remove the list item's bullet
- // so it is seen as the next token.
- space = item.length;
- item = item.replace(/^ *([*+-]|\d+\.) +/, '');
-
- // Outdent whatever the
- // list item contains. Hacky.
- if (~item.indexOf('\n ')) {
- space -= item.length;
- item = !this.options.pedantic
- ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
- : item.replace(/^ {1,4}/gm, '');
- }
-
- // Determine whether the next list item belongs here.
- // Backpedal if it does not belong in this list.
- if (this.options.smartLists && i !== l - 1) {
- b = block.bullet.exec(cap[i + 1])[0];
- if (bull !== b && !(bull.length > 1 && b.length > 1)) {
- src = cap.slice(i + 1).join('\n') + src;
- i = l - 1;
- }
- }
-
- // Determine whether item is loose or not.
- // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
- // for discount behavior.
- loose = next || /\n\n(?!\s*$)/.test(item);
- if (i !== l - 1) {
- next = item.charAt(item.length - 1) === '\n';
- if (!loose) loose = next;
- }
-
- this.tokens.push({
- type: loose
- ? 'loose_item_start'
- : 'list_item_start'
- });
-
- // Recurse.
- this.token(item, false, bq);
-
- this.tokens.push({
- type: 'list_item_end'
- });
- }
-
- this.tokens.push({
- type: 'list_end'
- });
-
- continue;
- }
-
- // html
- if (cap = this.rules.html.exec(src)) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: this.options.sanitize
- ? 'paragraph'
- : 'html',
- pre: !this.options.sanitizer
- && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
- text: cap[0]
- });
- continue;
- }
-
- // def
- if ((!bq && top) && (cap = this.rules.def.exec(src))) {
- src = src.substring(cap[0].length);
- this.tokens.links[cap[1].toLowerCase()] = {
- href: cap[2],
- title: cap[3]
- };
- continue;
- }
-
- // table (gfm)
- if (top && (cap = this.rules.table.exec(src))) {
- src = src.substring(cap[0].length);
-
- item = {
- type: 'table',
- header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
- align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
- cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
- };
-
- for (i = 0; i < item.align.length; i++) {
- if (/^ *-+: *$/.test(item.align[i])) {
- item.align[i] = 'right';
- } else if (/^ *:-+: *$/.test(item.align[i])) {
- item.align[i] = 'center';
- } else if (/^ *:-+ *$/.test(item.align[i])) {
- item.align[i] = 'left';
- } else {
- item.align[i] = null;
- }
- }
-
- for (i = 0; i < item.cells.length; i++) {
- item.cells[i] = item.cells[i]
- .replace(/^ *\| *| *\| *$/g, '')
- .split(/ *\| */);
- }
-
- this.tokens.push(item);
-
- continue;
- }
-
- // top-level paragraph
- if (top && (cap = this.rules.paragraph.exec(src))) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'paragraph',
- text: cap[1].charAt(cap[1].length - 1) === '\n'
- ? cap[1].slice(0, -1)
- : cap[1]
- });
- continue;
- }
-
- // text
- if (cap = this.rules.text.exec(src)) {
- // Top-level should never reach here.
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'text',
- text: cap[0]
- });
- continue;
- }
-
- if (src) {
- throw new
- Error('Infinite loop on byte: ' + src.charCodeAt(0));
- }
- }
-
- return this.tokens;
-};
-
-/**
- * Inline-Level Grammar
- */
-
-var inline = {
- escape: /^\\([\\`*{}\[\]()#+\-.!_>])/,
- autolink: /^<([^ >]+(@|:\/)[^ >]+)>/,
- url: noop,
- tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
- link: /^!?\[(inside)\]\(href\)/,
- reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/,
- nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
- strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,
- em: /^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
- code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,
- br: /^ {2,}\n(?!\s*$)/,
- del: noop,
- text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/
-};
-
-inline._inside = /(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/;
-inline._href = /\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/;
-
-inline.link = replace(inline.link)
- ('inside', inline._inside)
- ('href', inline._href)
- ();
-
-inline.reflink = replace(inline.reflink)
- ('inside', inline._inside)
- ();
-
-/**
- * Normal Inline Grammar
- */
-
-inline.normal = merge({}, inline);
-
-/**
- * Pedantic Inline Grammar
- */
-
-inline.pedantic = merge({}, inline.normal, {
- strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,
- em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/
-});
-
-/**
- * GFM Inline Grammar
- */
-
-inline.gfm = merge({}, inline.normal, {
- escape: replace(inline.escape)('])', '~|])')(),
- url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,
- del: /^~~(?=\S)([\s\S]*?\S)~~/,
- text: replace(inline.text)
- (']|', '~]|')
- ('|', '|https?://|')
- ()
-});
-
-/**
- * GFM + Line Breaks Inline Grammar
- */
-
-inline.breaks = merge({}, inline.gfm, {
- br: replace(inline.br)('{2,}', '*')(),
- text: replace(inline.gfm.text)('{2,}', '*')()
-});
-
-/**
- * Inline Lexer & Compiler
- */
-
-function InlineLexer(links, options) {
- this.options = options || marked.defaults;
- this.links = links;
- this.rules = inline.normal;
- this.renderer = this.options.renderer || new Renderer;
- this.renderer.options = this.options;
-
- if (!this.links) {
- throw new
- Error('Tokens array requires a `links` property.');
- }
-
- if (this.options.gfm) {
- if (this.options.breaks) {
- this.rules = inline.breaks;
- } else {
- this.rules = inline.gfm;
- }
- } else if (this.options.pedantic) {
- this.rules = inline.pedantic;
- }
-}
-
-/**
- * Expose Inline Rules
- */
-
-InlineLexer.rules = inline;
-
-/**
- * Static Lexing/Compiling Method
- */
-
-InlineLexer.output = function(src, links, options) {
- var inline = new InlineLexer(links, options);
- return inline.output(src);
-};
-
-/**
- * Lexing/Compiling
- */
-
-InlineLexer.prototype.output = function(src) {
- var out = ''
- , link
- , text
- , href
- , cap;
-
- while (src) {
- // escape
- if (cap = this.rules.escape.exec(src)) {
- src = src.substring(cap[0].length);
- out += cap[1];
- continue;
- }
-
- // autolink
- if (cap = this.rules.autolink.exec(src)) {
- src = src.substring(cap[0].length);
- if (cap[2] === '@') {
- text = cap[1].charAt(6) === ':'
- ? this.mangle(cap[1].substring(7))
- : this.mangle(cap[1]);
- href = this.mangle('mailto:') + text;
- } else {
- text = escape(cap[1]);
- href = text;
- }
- out += this.renderer.link(href, null, text);
- continue;
- }
-
- // url (gfm)
- if (!this.inLink && (cap = this.rules.url.exec(src))) {
- src = src.substring(cap[0].length);
- text = escape(cap[1]);
- href = text;
- out += this.renderer.link(href, null, text);
- continue;
- }
-
- // tag
- if (cap = this.rules.tag.exec(src)) {
- if (!this.inLink && /^<a /i.test(cap[0])) {
- this.inLink = true;
- } else if (this.inLink && /^<\/a>/i.test(cap[0])) {
- this.inLink = false;
- }
- src = src.substring(cap[0].length);
- out += this.options.sanitize
- ? this.options.sanitizer
- ? this.options.sanitizer(cap[0])
- : escape(cap[0])
- : cap[0]
- continue;
- }
-
- // link
- if (cap = this.rules.link.exec(src)) {
- src = src.substring(cap[0].length);
- this.inLink = true;
- out += this.outputLink(cap, {
- href: cap[2],
- title: cap[3]
- });
- this.inLink = false;
- continue;
- }
-
- // reflink, nolink
- if ((cap = this.rules.reflink.exec(src))
- || (cap = this.rules.nolink.exec(src))) {
- src = src.substring(cap[0].length);
- link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
- link = this.links[link.toLowerCase()];
- if (!link || !link.href) {
- out += cap[0].charAt(0);
- src = cap[0].substring(1) + src;
- continue;
- }
- this.inLink = true;
- out += this.outputLink(cap, link);
- this.inLink = false;
- continue;
- }
-
- // strong
- if (cap = this.rules.strong.exec(src)) {
- src = src.substring(cap[0].length);
- out += this.renderer.strong(this.output(cap[2] || cap[1]));
- continue;
- }
-
- // em
- if (cap = this.rules.em.exec(src)) {
- src = src.substring(cap[0].length);
- out += this.renderer.em(this.output(cap[2] || cap[1]));
- continue;
- }
-
- // code
- if (cap = this.rules.code.exec(src)) {
- src = src.substring(cap[0].length);
- out += this.renderer.codespan(escape(cap[2], true));
- continue;
- }
-
- // br
- if (cap = this.rules.br.exec(src)) {
- src = src.substring(cap[0].length);
- out += this.renderer.br();
- continue;
- }
-
- // del (gfm)
- if (cap = this.rules.del.exec(src)) {
- src = src.substring(cap[0].length);
- out += this.renderer.del(this.output(cap[1]));
- continue;
- }
-
- // text
- if (cap = this.rules.text.exec(src)) {
- src = src.substring(cap[0].length);
- out += this.renderer.text(escape(this.smartypants(cap[0])));
- continue;
- }
-
- if (src) {
- throw new
- Error('Infinite loop on byte: ' + src.charCodeAt(0));
- }
- }
-
- return out;
-};
-
-/**
- * Compile Link
- */
-
-InlineLexer.prototype.outputLink = function(cap, link) {
- var href = escape(link.href)
- , title = link.title ? escape(link.title) : null;
-
- return cap[0].charAt(0) !== '!'
- ? this.renderer.link(href, title, this.output(cap[1]))
- : this.renderer.image(href, title, escape(cap[1]));
-};
-
-/**
- * Smartypants Transformations
- */
-
-InlineLexer.prototype.smartypants = function(text) {
- if (!this.options.smartypants) return text;
- return text
- // em-dashes
- .replace(/---/g, '\u2014')
- // en-dashes
- .replace(/--/g, '\u2013')
- // opening singles
- .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
- // closing singles & apostrophes
- .replace(/'/g, '\u2019')
- // opening doubles
- .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
- // closing doubles
- .replace(/"/g, '\u201d')
- // ellipses
- .replace(/\.{3}/g, '\u2026');
-};
-
-/**
- * Mangle Links
- */
-
-InlineLexer.prototype.mangle = function(text) {
- if (!this.options.mangle) return text;
- var out = ''
- , l = text.length
- , i = 0
- , ch;
-
- for (; i < l; i++) {
- ch = text.charCodeAt(i);
- if (Math.random() > 0.5) {
- ch = 'x' + ch.toString(16);
- }
- out += '&#' + ch + ';';
- }
-
- return out;
-};
-
-/**
- * Renderer
- */
-
-function Renderer(options) {
- this.options = options || {};
-}
-
-Renderer.prototype.code = function(code, lang, escaped) {
- if (this.options.highlight) {
- var out = this.options.highlight(code, lang);
- if (out != null && out !== code) {
- escaped = true;
- code = out;
- }
- }
-
- if (!lang) {
- return '<pre><code>'
- + (escaped ? code : escape(code, true))
- + '\n</code></pre>';
- }
-
- return '<pre><code class="'
- + this.options.langPrefix
- + escape(lang, true)
- + '">'
- + (escaped ? code : escape(code, true))
- + '\n</code></pre>\n';
-};
-
-Renderer.prototype.blockquote = function(quote) {
- return '<blockquote>\n' + quote + '</blockquote>\n';
-};
-
-Renderer.prototype.html = function(html) {
- return html;
-};
-
-Renderer.prototype.heading = function(text, level, raw) {
- return '<h'
- + level
- + ' id="'
- + this.options.headerPrefix
- + raw.toLowerCase().replace(/[^\w]+/g, '-')
- + '">'
- + text
- + '</h'
- + level
- + '>\n';
-};
-
-Renderer.prototype.hr = function() {
- return this.options.xhtml ? '<hr/>\n' : '<hr>\n';
-};
-
-Renderer.prototype.list = function(body, ordered) {
- var type = ordered ? 'ol' : 'ul';
- return '<' + type + '>\n' + body + '</' + type + '>\n';
-};
-
-Renderer.prototype.listitem = function(text) {
- return '<li>' + text + '</li>\n';
-};
-
-Renderer.prototype.paragraph = function(text) {
- return '<p>' + text + '</p>\n';
-};
-
-Renderer.prototype.table = function(header, body) {
- return '<table>\n'
- + '<thead>\n'
- + header
- + '</thead>\n'
- + '<tbody>\n'
- + body
- + '</tbody>\n'
- + '</table>\n';
-};
-
-Renderer.prototype.tablerow = function(content) {
- return '<tr>\n' + content + '</tr>\n';
-};
-
-Renderer.prototype.tablecell = function(content, flags) {
- var type = flags.header ? 'th' : 'td';
- var tag = flags.align
- ? '<' + type + ' style="text-align:' + flags.align + '">'
- : '<' + type + '>';
- return tag + content + '</' + type + '>\n';
-};
-
-// span level renderer
-Renderer.prototype.strong = function(text) {
- return '<strong>' + text + '</strong>';
-};
-
-Renderer.prototype.em = function(text) {
- return '<em>' + text + '</em>';
-};
-
-Renderer.prototype.codespan = function(text) {
- return '<code>' + text + '</code>';
-};
-
-Renderer.prototype.br = function() {
- return this.options.xhtml ? '<br/>' : '<br>';
-};
-
-Renderer.prototype.del = function(text) {
- return '<del>' + text + '</del>';
-};
-
-Renderer.prototype.link = function(href, title, text) {
- if (this.options.sanitize) {
- try {
- var prot = decodeURIComponent(unescape(href))
- .replace(/[^\w:]/g, '')
- .toLowerCase();
- } catch (e) {
- return '';
- }
- if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0) {
- return '';
- }
- }
- var out = '<a href="' + href + '"';
- if (title) {
- out += ' title="' + title + '"';
- }
- out += '>' + text + '</a>';
- return out;
-};
-
-Renderer.prototype.image = function(href, title, text) {
- var out = '<img src="' + href + '" alt="' + text + '"';
- if (title) {
- out += ' title="' + title + '"';
- }
- out += this.options.xhtml ? '/>' : '>';
- return out;
-};
-
-Renderer.prototype.text = function(text) {
- return text;
-};
-
-/**
- * Parsing & Compiling
- */
-
-function Parser(options) {
- this.tokens = [];
- this.token = null;
- this.options = options || marked.defaults;
- this.options.renderer = this.options.renderer || new Renderer;
- this.renderer = this.options.renderer;
- this.renderer.options = this.options;
-}
-
-/**
- * Static Parse Method
- */
-
-Parser.parse = function(src, options, renderer) {
- var parser = new Parser(options, renderer);
- return parser.parse(src);
-};
-
-/**
- * Parse Loop
- */
-
-Parser.prototype.parse = function(src) {
- this.inline = new InlineLexer(src.links, this.options, this.renderer);
- this.tokens = src.reverse();
-
- var out = '';
- while (this.next()) {
- out += this.tok();
- }
-
- return out;
-};
-
-/**
- * Next Token
- */
-
-Parser.prototype.next = function() {
- return this.token = this.tokens.pop();
-};
-
-/**
- * Preview Next Token
- */
-
-Parser.prototype.peek = function() {
- return this.tokens[this.tokens.length - 1] || 0;
-};
-
-/**
- * Parse Text Tokens
- */
-
-Parser.prototype.parseText = function() {
- var body = this.token.text;
-
- while (this.peek().type === 'text') {
- body += '\n' + this.next().text;
- }
-
- return this.inline.output(body);
-};
-
-/**
- * Parse Current Token
- */
-
-Parser.prototype.tok = function() {
- switch (this.token.type) {
- case 'space': {
- return '';
- }
- case 'hr': {
- return this.renderer.hr();
- }
- case 'heading': {
- return this.renderer.heading(
- this.inline.output(this.token.text),
- this.token.depth,
- this.token.text);
- }
- case 'code': {
- return this.renderer.code(this.token.text,
- this.token.lang,
- this.token.escaped);
- }
- case 'table': {
- var header = ''
- , body = ''
- , i
- , row
- , cell
- , flags
- , j;
-
- // header
- cell = '';
- for (i = 0; i < this.token.header.length; i++) {
- flags = { header: true, align: this.token.align[i] };
- cell += this.renderer.tablecell(
- this.inline.output(this.token.header[i]),
- { header: true, align: this.token.align[i] }
- );
- }
- header += this.renderer.tablerow(cell);
-
- for (i = 0; i < this.token.cells.length; i++) {
- row = this.token.cells[i];
-
- cell = '';
- for (j = 0; j < row.length; j++) {
- cell += this.renderer.tablecell(
- this.inline.output(row[j]),
- { header: false, align: this.token.align[j] }
- );
- }
-
- body += this.renderer.tablerow(cell);
- }
- return this.renderer.table(header, body);
- }
- case 'blockquote_start': {
- var body = '';
-
- while (this.next().type !== 'blockquote_end') {
- body += this.tok();
- }
-
- return this.renderer.blockquote(body);
- }
- case 'list_start': {
- var body = ''
- , ordered = this.token.ordered;
-
- while (this.next().type !== 'list_end') {
- body += this.tok();
- }
-
- return this.renderer.list(body, ordered);
- }
- case 'list_item_start': {
- var body = '';
-
- while (this.next().type !== 'list_item_end') {
- body += this.token.type === 'text'
- ? this.parseText()
- : this.tok();
- }
-
- return this.renderer.listitem(body);
- }
- case 'loose_item_start': {
- var body = '';
-
- while (this.next().type !== 'list_item_end') {
- body += this.tok();
- }
-
- return this.renderer.listitem(body);
- }
- case 'html': {
- var html = !this.token.pre && !this.options.pedantic
- ? this.inline.output(this.token.text)
- : this.token.text;
- return this.renderer.html(html);
- }
- case 'paragraph': {
- return this.renderer.paragraph(this.inline.output(this.token.text));
- }
- case 'text': {
- return this.renderer.paragraph(this.parseText());
- }
- }
-};
-
-/**
- * Helpers
- */
-
-function escape(html, encode) {
- return html
- .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&amp;')
- .replace(/</g, '&lt;')
- .replace(/>/g, '&gt;')
- .replace(/"/g, '&quot;')
- .replace(/'/g, '&#39;');
-}
-
-function unescape(html) {
- // explicitly match decimal, hex, and named HTML entities
- return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g, function(_, n) {
- n = n.toLowerCase();
- if (n === 'colon') return ':';
- if (n.charAt(0) === '#') {
- return n.charAt(1) === 'x'
- ? String.fromCharCode(parseInt(n.substring(2), 16))
- : String.fromCharCode(+n.substring(1));
- }
- return '';
- });
-}
-
-function replace(regex, opt) {
- regex = regex.source;
- opt = opt || '';
- return function self(name, val) {
- if (!name) return new RegExp(regex, opt);
- val = val.source || val;
- val = val.replace(/(^|[^\[])\^/g, '$1');
- regex = regex.replace(name, val);
- return self;
- };
-}
-
-function noop() {}
-noop.exec = noop;
-
-function merge(obj) {
- var i = 1
- , target
- , key;
-
- for (; i < arguments.length; i++) {
- target = arguments[i];
- for (key in target) {
- if (Object.prototype.hasOwnProperty.call(target, key)) {
- obj[key] = target[key];
- }
- }
- }
-
- return obj;
-}
-
-
-/**
- * Marked
- */
-
-function marked(src, opt, callback) {
- if (callback || typeof opt === 'function') {
- if (!callback) {
- callback = opt;
- opt = null;
- }
-
- opt = merge({}, marked.defaults, opt || {});
-
- var highlight = opt.highlight
- , tokens
- , pending
- , i = 0;
-
- try {
- tokens = Lexer.lex(src, opt)
- } catch (e) {
- return callback(e);
- }
-
- pending = tokens.length;
-
- var done = function(err) {
- if (err) {
- opt.highlight = highlight;
- return callback(err);
- }
-
- var out;
-
- try {
- out = Parser.parse(tokens, opt);
- } catch (e) {
- err = e;
- }
-
- opt.highlight = highlight;
-
- return err
- ? callback(err)
- : callback(null, out);
- };
-
- if (!highlight || highlight.length < 3) {
- return done();
- }
-
- delete opt.highlight;
-
- if (!pending) return done();
-
- for (; i < tokens.length; i++) {
- (function(token) {
- if (token.type !== 'code') {
- return --pending || done();
- }
- return highlight(token.text, token.lang, function(err, code) {
- if (err) return done(err);
- if (code == null || code === token.text) {
- return --pending || done();
- }
- token.text = code;
- token.escaped = true;
- --pending || done();
- });
- })(tokens[i]);
- }
-
- return;
- }
- try {
- if (opt) opt = merge({}, marked.defaults, opt);
- return Parser.parse(Lexer.lex(src, opt), opt);
- } catch (e) {
- e.message += '\nPlease report this to https://github.com/chjj/marked.';
- if ((opt || marked.defaults).silent) {
- return '<p>An error occured:</p><pre>'
- + escape(e.message + '', true)
- + '</pre>';
- }
- throw e;
- }
-}
-
-/**
- * Options
- */
-
-marked.options =
-marked.setOptions = function(opt) {
- merge(marked.defaults, opt);
- return marked;
-};
-
-marked.defaults = {
- gfm: true,
- tables: true,
- breaks: false,
- pedantic: false,
- sanitize: false,
- sanitizer: null,
- mangle: true,
- smartLists: false,
- silent: false,
- highlight: null,
- langPrefix: 'lang-',
- smartypants: false,
- headerPrefix: '',
- renderer: new Renderer,
- xhtml: false
-};
-
-/**
- * Expose
- */
-
-marked.Parser = Parser;
-marked.parser = Parser.parse;
-
-marked.Renderer = Renderer;
-
-marked.Lexer = Lexer;
-marked.lexer = Lexer.lex;
-
-marked.InlineLexer = InlineLexer;
-marked.inlineLexer = InlineLexer.output;
-
-marked.parse = marked;
-
-if (true) {
- module.exports = marked;
-} else if (typeof define === 'function' && define.amd) {
- define(function() { return marked; });
-} else {
- this.marked = marked;
-}
-
-}).call(function() {
- return this || (typeof window !== 'undefined' ? window : global);
-}());
-
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
-
-/***/ }),
-/* 26 */
-/***/ (function(module, exports) {
-
-Prism.languages.python= {
- 'triple-quoted-string': {
- pattern: /"""[\s\S]+?"""|'''[\s\S]+?'''/,
- alias: 'string'
- },
- 'comment': {
- pattern: /(^|[^\\])#.*/,
- lookbehind: true
- },
- 'string': {
- pattern: /("|')(?:\\\\|\\?[^\\\r\n])*?\1/,
- greedy: true
- },
- 'function' : {
- pattern: /((?:^|\s)def[ \t]+)[a-zA-Z_][a-zA-Z0-9_]*(?=\()/g,
- lookbehind: true
- },
- 'class-name': {
- pattern: /(\bclass\s+)[a-z0-9_]+/i,
- lookbehind: true
- },
- 'keyword' : /\b(?:as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|pass|print|raise|return|try|while|with|yield)\b/,
- 'boolean' : /\b(?:True|False)\b/,
- 'number' : /\b-?(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,
- 'operator' : /[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]|\b(?:or|and|not)\b/,
- 'punctuation' : /[{}[\];(),.:]/
-};
-
-
-/***/ }),
-/* 27 */
-/***/ (function(module, exports, __webpack_require__) {
-
-/* WEBPACK VAR INJECTION */(function(global) {(function(){
-
-if (
- (typeof self === 'undefined' || !self.Prism) &&
- (typeof global === 'undefined' || !global.Prism)
-) {
- return;
-}
-
-var options = {};
-Prism.plugins.customClass = {
- map: function map(cm) {
- options.classMap = cm;
- },
- prefix: function prefix(string) {
- options.prefixString = string;
- }
-}
-
-Prism.hooks.add('wrap', function (env) {
- if (!options.classMap && !options.prefixString) {
- return;
- }
- env.classes = env.classes.map(function(c) {
- return (options.prefixString || '') + (options.classMap[c] || c);
- });
-});
-
-})();
-
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
-
-/***/ }),
-/* 28 */
-/***/ (function(module, exports, __webpack_require__) {
-
-/* WEBPACK VAR INJECTION */(function(global) {
-/* **********************************************
- Begin prism-core.js
-********************************************** */
-
-var _self = (typeof window !== 'undefined')
- ? window // if in browser
- : (
- (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)
- ? self // if in worker
- : {} // if in node js
- );
-
-/**
- * Prism: Lightweight, robust, elegant syntax highlighting
- * MIT license http://www.opensource.org/licenses/mit-license.php/
- * @author Lea Verou http://lea.verou.me
- */
-
-var Prism = (function(){
-
-// Private helper vars
-var lang = /\blang(?:uage)?-(\w+)\b/i;
-var uniqueId = 0;
-
-var _ = _self.Prism = {
- util: {
- encode: function (tokens) {
- if (tokens instanceof Token) {
- return new Token(tokens.type, _.util.encode(tokens.content), tokens.alias);
- } else if (_.util.type(tokens) === 'Array') {
- return tokens.map(_.util.encode);
- } else {
- return tokens.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ');
- }
- },
-
- type: function (o) {
- return Object.prototype.toString.call(o).match(/\[object (\w+)\]/)[1];
- },
-
- objId: function (obj) {
- if (!obj['__id']) {
- Object.defineProperty(obj, '__id', { value: ++uniqueId });
- }
- return obj['__id'];
- },
-
- // Deep clone a language definition (e.g. to extend it)
- clone: function (o) {
- var type = _.util.type(o);
-
- switch (type) {
- case 'Object':
- var clone = {};
-
- for (var key in o) {
- if (o.hasOwnProperty(key)) {
- clone[key] = _.util.clone(o[key]);
- }
- }
-
- return clone;
-
- case 'Array':
- // Check for existence for IE8
- return o.map && o.map(function(v) { return _.util.clone(v); });
- }
-
- return o;
- }
- },
-
- languages: {
- extend: function (id, redef) {
- var lang = _.util.clone(_.languages[id]);
-
- for (var key in redef) {
- lang[key] = redef[key];
- }
-
- return lang;
- },
-
- /**
- * Insert a token before another token in a language literal
- * As this needs to recreate the object (we cannot actually insert before keys in object literals),
- * we cannot just provide an object, we need anobject and a key.
- * @param inside The key (or language id) of the parent
- * @param before The key to insert before. If not provided, the function appends instead.
- * @param insert Object with the key/value pairs to insert
- * @param root The object that contains `inside`. If equal to Prism.languages, it can be omitted.
- */
- insertBefore: function (inside, before, insert, root) {
- root = root || _.languages;
- var grammar = root[inside];
-
- if (arguments.length == 2) {
- insert = arguments[1];
-
- for (var newToken in insert) {
- if (insert.hasOwnProperty(newToken)) {
- grammar[newToken] = insert[newToken];
- }
- }
-
- return grammar;
- }
-
- var ret = {};
-
- for (var token in grammar) {
-
- if (grammar.hasOwnProperty(token)) {
-
- if (token == before) {
-
- for (var newToken in insert) {
-
- if (insert.hasOwnProperty(newToken)) {
- ret[newToken] = insert[newToken];
- }
- }
- }
-
- ret[token] = grammar[token];
- }
- }
-
- // Update references in other language definitions
- _.languages.DFS(_.languages, function(key, value) {
- if (value === root[inside] && key != inside) {
- this[key] = ret;
- }
- });
-
- return root[inside] = ret;
- },
-
- // Traverse a language definition with Depth First Search
- DFS: function(o, callback, type, visited) {
- visited = visited || {};
- for (var i in o) {
- if (o.hasOwnProperty(i)) {
- callback.call(o, i, o[i], type || i);
-
- if (_.util.type(o[i]) === 'Object' && !visited[_.util.objId(o[i])]) {
- visited[_.util.objId(o[i])] = true;
- _.languages.DFS(o[i], callback, null, visited);
- }
- else if (_.util.type(o[i]) === 'Array' && !visited[_.util.objId(o[i])]) {
- visited[_.util.objId(o[i])] = true;
- _.languages.DFS(o[i], callback, i, visited);
- }
- }
- }
- }
- },
- plugins: {},
-
- highlightAll: function(async, callback) {
- var env = {
- callback: callback,
- selector: 'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'
- };
-
- _.hooks.run("before-highlightall", env);
-
- var elements = env.elements || document.querySelectorAll(env.selector);
-
- for (var i=0, element; element = elements[i++];) {
- _.highlightElement(element, async === true, env.callback);
- }
- },
-
- highlightElement: function(element, async, callback) {
- // Find language
- var language, grammar, parent = element;
-
- while (parent && !lang.test(parent.className)) {
- parent = parent.parentNode;
- }
-
- if (parent) {
- language = (parent.className.match(lang) || [,''])[1].toLowerCase();
- grammar = _.languages[language];
- }
-
- // Set language on the element, if not present
- element.className = element.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
-
- // Set language on the parent, for styling
- parent = element.parentNode;
-
- if (/pre/i.test(parent.nodeName)) {
- parent.className = parent.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
- }
-
- var code = element.textContent;
-
- var env = {
- element: element,
- language: language,
- grammar: grammar,
- code: code
- };
-
- _.hooks.run('before-sanity-check', env);
-
- if (!env.code || !env.grammar) {
- if (env.code) {
- env.element.textContent = env.code;
- }
- _.hooks.run('complete', env);
- return;
- }
-
- _.hooks.run('before-highlight', env);
-
- if (async && _self.Worker) {
- var worker = new Worker(_.filename);
-
- worker.onmessage = function(evt) {
- env.highlightedCode = evt.data;
-
- _.hooks.run('before-insert', env);
-
- env.element.innerHTML = env.highlightedCode;
-
- callback && callback.call(env.element);
- _.hooks.run('after-highlight', env);
- _.hooks.run('complete', env);
- };
-
- worker.postMessage(JSON.stringify({
- language: env.language,
- code: env.code,
- immediateClose: true
- }));
- }
- else {
- env.highlightedCode = _.highlight(env.code, env.grammar, env.language);
-
- _.hooks.run('before-insert', env);
-
- env.element.innerHTML = env.highlightedCode;
-
- callback && callback.call(element);
-
- _.hooks.run('after-highlight', env);
- _.hooks.run('complete', env);
- }
- },
-
- highlight: function (text, grammar, language) {
- var tokens = _.tokenize(text, grammar);
- return Token.stringify(_.util.encode(tokens), language);
- },
-
- tokenize: function(text, grammar, language) {
- var Token = _.Token;
-
- var strarr = [text];
-
- var rest = grammar.rest;
-
- if (rest) {
- for (var token in rest) {
- grammar[token] = rest[token];
- }
-
- delete grammar.rest;
- }
-
- tokenloop: for (var token in grammar) {
- if(!grammar.hasOwnProperty(token) || !grammar[token]) {
- continue;
- }
-
- var patterns = grammar[token];
- patterns = (_.util.type(patterns) === "Array") ? patterns : [patterns];
-
- for (var j = 0; j < patterns.length; ++j) {
- var pattern = patterns[j],
- inside = pattern.inside,
- lookbehind = !!pattern.lookbehind,
- greedy = !!pattern.greedy,
- lookbehindLength = 0,
- alias = pattern.alias;
-
- if (greedy && !pattern.pattern.global) {
- // Without the global flag, lastIndex won't work
- var flags = pattern.pattern.toString().match(/[imuy]*$/)[0];
- pattern.pattern = RegExp(pattern.pattern.source, flags + "g");
- }
-
- pattern = pattern.pattern || pattern;
-
- // Don’t cache length as it changes during the loop
- for (var i=0, pos = 0; i<strarr.length; pos += strarr[i].length, ++i) {
-
- var str = strarr[i];
-
- if (strarr.length > text.length) {
- // Something went terribly wrong, ABORT, ABORT!
- break tokenloop;
- }
-
- if (str instanceof Token) {
- continue;
- }
-
- pattern.lastIndex = 0;
-
- var match = pattern.exec(str),
- delNum = 1;
-
- // Greedy patterns can override/remove up to two previously matched tokens
- if (!match && greedy && i != strarr.length - 1) {
- pattern.lastIndex = pos;
- match = pattern.exec(text);
- if (!match) {
- break;
- }
-
- var from = match.index + (lookbehind ? match[1].length : 0),
- to = match.index + match[0].length,
- k = i,
- p = pos;
-
- for (var len = strarr.length; k < len && p < to; ++k) {
- p += strarr[k].length;
- // Move the index i to the element in strarr that is closest to from
- if (from >= p) {
- ++i;
- pos = p;
- }
- }
-
- /*
- * If strarr[i] is a Token, then the match starts inside another Token, which is invalid
- * If strarr[k - 1] is greedy we are in conflict with another greedy pattern
- */
- if (strarr[i] instanceof Token || strarr[k - 1].greedy) {
- continue;
- }
-
- // Number of tokens to delete and replace with the new match
- delNum = k - i;
- str = text.slice(pos, p);
- match.index -= pos;
- }
-
- if (!match) {
- continue;
- }
-
- if(lookbehind) {
- lookbehindLength = match[1].length;
- }
-
- var from = match.index + lookbehindLength,
- match = match[0].slice(lookbehindLength),
- to = from + match.length,
- before = str.slice(0, from),
- after = str.slice(to);
-
- var args = [i, delNum];
-
- if (before) {
- args.push(before);
- }
-
- var wrapped = new Token(token, inside? _.tokenize(match, inside) : match, alias, match, greedy);
-
- args.push(wrapped);
-
- if (after) {
- args.push(after);
- }
-
- Array.prototype.splice.apply(strarr, args);
- }
- }
- }
-
- return strarr;
- },
-
- hooks: {
- all: {},
-
- add: function (name, callback) {
- var hooks = _.hooks.all;
-
- hooks[name] = hooks[name] || [];
-
- hooks[name].push(callback);
- },
-
- run: function (name, env) {
- var callbacks = _.hooks.all[name];
-
- if (!callbacks || !callbacks.length) {
- return;
- }
-
- for (var i=0, callback; callback = callbacks[i++];) {
- callback(env);
- }
- }
- }
-};
-
-var Token = _.Token = function(type, content, alias, matchedStr, greedy) {
- this.type = type;
- this.content = content;
- this.alias = alias;
- // Copy of the full string this token was created from
- this.length = (matchedStr || "").length|0;
- this.greedy = !!greedy;
-};
-
-Token.stringify = function(o, language, parent) {
- if (typeof o == 'string') {
- return o;
- }
-
- if (_.util.type(o) === 'Array') {
- return o.map(function(element) {
- return Token.stringify(element, language, o);
- }).join('');
- }
-
- var env = {
- type: o.type,
- content: Token.stringify(o.content, language, parent),
- tag: 'span',
- classes: ['token', o.type],
- attributes: {},
- language: language,
- parent: parent
- };
-
- if (env.type == 'comment') {
- env.attributes['spellcheck'] = 'true';
- }
-
- if (o.alias) {
- var aliases = _.util.type(o.alias) === 'Array' ? o.alias : [o.alias];
- Array.prototype.push.apply(env.classes, aliases);
- }
-
- _.hooks.run('wrap', env);
-
- var attributes = Object.keys(env.attributes).map(function(name) {
- return name + '="' + (env.attributes[name] || '').replace(/"/g, '&quot;') + '"';
- }).join(' ');
-
- return '<' + env.tag + ' class="' + env.classes.join(' ') + '"' + (attributes ? ' ' + attributes : '') + '>' + env.content + '</' + env.tag + '>';
-
-};
-
-if (!_self.document) {
- if (!_self.addEventListener) {
- // in Node.js
- return _self.Prism;
- }
- // In worker
- _self.addEventListener('message', function(evt) {
- var message = JSON.parse(evt.data),
- lang = message.language,
- code = message.code,
- immediateClose = message.immediateClose;
-
- _self.postMessage(_.highlight(code, _.languages[lang], lang));
- if (immediateClose) {
- _self.close();
- }
- }, false);
-
- return _self.Prism;
-}
-
-//Get current script and highlight
-var script = document.currentScript || [].slice.call(document.getElementsByTagName("script")).pop();
-
-if (script) {
- _.filename = script.src;
-
- if (document.addEventListener && !script.hasAttribute('data-manual')) {
- if(document.readyState !== "loading") {
- if (window.requestAnimationFrame) {
- window.requestAnimationFrame(_.highlightAll);
- } else {
- window.setTimeout(_.highlightAll, 16);
- }
- }
- else {
- document.addEventListener('DOMContentLoaded', _.highlightAll);
- }
- }
-}
-
-return _self.Prism;
-
-})();
-
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = Prism;
-}
-
-// hack for components to work correctly in node.js
-if (typeof global !== 'undefined') {
- global.Prism = Prism;
-}
-
-
-/* **********************************************
- Begin prism-markup.js
-********************************************** */
-
-Prism.languages.markup = {
- 'comment': /<!--[\w\W]*?-->/,
- 'prolog': /<\?[\w\W]+?\?>/,
- 'doctype': /<!DOCTYPE[\w\W]+?>/i,
- 'cdata': /<!\[CDATA\[[\w\W]*?]]>/i,
- 'tag': {
- pattern: /<\/?(?!\d)[^\s>\/=$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,
- inside: {
- 'tag': {
- pattern: /^<\/?[^\s>\/]+/i,
- inside: {
- 'punctuation': /^<\/?/,
- 'namespace': /^[^\s>\/:]+:/
- }
- },
- 'attr-value': {
- pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,
- inside: {
- 'punctuation': /[=>"']/
- }
- },
- 'punctuation': /\/?>/,
- 'attr-name': {
- pattern: /[^\s>\/]+/,
- inside: {
- 'namespace': /^[^\s>\/:]+:/
- }
- }
-
- }
- },
- 'entity': /&#?[\da-z]{1,8};/i
-};
-
-// Plugin to make entity title show the real entity, idea by Roman Komarov
-Prism.hooks.add('wrap', function(env) {
-
- if (env.type === 'entity') {
- env.attributes['title'] = env.content.replace(/&amp;/, '&');
- }
-});
-
-Prism.languages.xml = Prism.languages.markup;
-Prism.languages.html = Prism.languages.markup;
-Prism.languages.mathml = Prism.languages.markup;
-Prism.languages.svg = Prism.languages.markup;
-
-
-/* **********************************************
- Begin prism-css.js
-********************************************** */
-
-Prism.languages.css = {
- 'comment': /\/\*[\w\W]*?\*\//,
- 'atrule': {
- pattern: /@[\w-]+?.*?(;|(?=\s*\{))/i,
- inside: {
- 'rule': /@[\w-]+/
- // See rest below
- }
- },
- 'url': /url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,
- 'selector': /[^\{\}\s][^\{\};]*?(?=\s*\{)/,
- 'string': {
- pattern: /("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,
- greedy: true
- },
- 'property': /(\b|\B)[\w-]+(?=\s*:)/i,
- 'important': /\B!important\b/i,
- 'function': /[-a-z0-9]+(?=\()/i,
- 'punctuation': /[(){};:]/
-};
-
-Prism.languages.css['atrule'].inside.rest = Prism.util.clone(Prism.languages.css);
-
-if (Prism.languages.markup) {
- Prism.languages.insertBefore('markup', 'tag', {
- 'style': {
- pattern: /(<style[\w\W]*?>)[\w\W]*?(?=<\/style>)/i,
- lookbehind: true,
- inside: Prism.languages.css,
- alias: 'language-css'
- }
- });
-
- Prism.languages.insertBefore('inside', 'attr-value', {
- 'style-attr': {
- pattern: /\s*style=("|').*?\1/i,
- inside: {
- 'attr-name': {
- pattern: /^\s*style/i,
- inside: Prism.languages.markup.tag.inside
- },
- 'punctuation': /^\s*=\s*['"]|['"]\s*$/,
- 'attr-value': {
- pattern: /.+/i,
- inside: Prism.languages.css
- }
- },
- alias: 'language-css'
- }
- }, Prism.languages.markup.tag);
-}
-
-/* **********************************************
- Begin prism-clike.js
-********************************************** */
-
-Prism.languages.clike = {
- 'comment': [
- {
- pattern: /(^|[^\\])\/\*[\w\W]*?\*\//,
- lookbehind: true
- },
- {
- pattern: /(^|[^\\:])\/\/.*/,
- lookbehind: true
- }
- ],
- 'string': {
- pattern: /(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
- greedy: true
- },
- 'class-name': {
- pattern: /((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,
- lookbehind: true,
- inside: {
- punctuation: /(\.|\\)/
- }
- },
- 'keyword': /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,
- 'boolean': /\b(true|false)\b/,
- 'function': /[a-z0-9_]+(?=\()/i,
- 'number': /\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,
- 'operator': /--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,
- 'punctuation': /[{}[\];(),.:]/
-};
-
-
-/* **********************************************
- Begin prism-javascript.js
-********************************************** */
-
-Prism.languages.javascript = Prism.languages.extend('clike', {
- 'keyword': /\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,
- 'number': /\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,
- // Allow for all non-ASCII characters (See http://stackoverflow.com/a/2008444)
- 'function': /[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i,
- 'operator': /--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*\*?|\/|~|\^|%|\.{3}/
-});
-
-Prism.languages.insertBefore('javascript', 'keyword', {
- 'regex': {
- pattern: /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,
- lookbehind: true,
- greedy: true
- }
-});
-
-Prism.languages.insertBefore('javascript', 'string', {
- 'template-string': {
- pattern: /`(?:\\\\|\\?[^\\])*?`/,
- greedy: true,
- inside: {
- 'interpolation': {
- pattern: /\$\{[^}]+\}/,
- inside: {
- 'interpolation-punctuation': {
- pattern: /^\$\{|\}$/,
- alias: 'punctuation'
- },
- rest: Prism.languages.javascript
- }
- },
- 'string': /[\s\S]+/
- }
- }
-});
-
-if (Prism.languages.markup) {
- Prism.languages.insertBefore('markup', 'tag', {
- 'script': {
- pattern: /(<script[\w\W]*?>)[\w\W]*?(?=<\/script>)/i,
- lookbehind: true,
- inside: Prism.languages.javascript,
- alias: 'language-javascript'
- }
- });
-}
-
-Prism.languages.js = Prism.languages.javascript;
-
-/* **********************************************
- Begin prism-file-highlight.js
-********************************************** */
-
-(function () {
- if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) {
- return;
- }
-
- self.Prism.fileHighlight = function() {
-
- var Extensions = {
- 'js': 'javascript',
- 'py': 'python',
- 'rb': 'ruby',
- 'ps1': 'powershell',
- 'psm1': 'powershell',
- 'sh': 'bash',
- 'bat': 'batch',
- 'h': 'c',
- 'tex': 'latex'
- };
-
- if(Array.prototype.forEach) { // Check to prevent error in IE8
- Array.prototype.slice.call(document.querySelectorAll('pre[data-src]')).forEach(function (pre) {
- var src = pre.getAttribute('data-src');
-
- var language, parent = pre;
- var lang = /\blang(?:uage)?-(?!\*)(\w+)\b/i;
- while (parent && !lang.test(parent.className)) {
- parent = parent.parentNode;
- }
-
- if (parent) {
- language = (pre.className.match(lang) || [, ''])[1];
- }
-
- if (!language) {
- var extension = (src.match(/\.(\w+)$/) || [, ''])[1];
- language = Extensions[extension] || extension;
- }
-
- var code = document.createElement('code');
- code.className = 'language-' + language;
-
- pre.textContent = '';
-
- code.textContent = 'Loading…';
-
- pre.appendChild(code);
-
- var xhr = new XMLHttpRequest();
-
- xhr.open('GET', src, true);
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
-
- if (xhr.status < 400 && xhr.responseText) {
- code.textContent = xhr.responseText;
-
- Prism.highlightElement(code);
- }
- else if (xhr.status >= 400) {
- code.textContent = '✖ Error ' + xhr.status + ' while fetching file: ' + xhr.statusText;
- }
- else {
- code.textContent = '✖ Error: File does not exist or is empty';
- }
- }
- };
-
- xhr.send(null);
- });
- }
-
- };
-
- document.addEventListener('DOMContentLoaded', self.Prism.fileHighlight);
-
-})();
-
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
-
-/***/ }),
-/* 29 */
-/***/ (function(module, exports, __webpack_require__) {
-
-
-/* styles */
-__webpack_require__(42)
-
-var Component = __webpack_require__(0)(
- /* script */
- __webpack_require__(7),
- /* template */
- __webpack_require__(36),
- /* scopeId */
- "data-v-3ac4c361",
- /* cssModules */
- null
-)
-
-module.exports = Component.exports
-
-
-/***/ }),
-/* 30 */
-/***/ (function(module, exports, __webpack_require__) {
-
-
-/* styles */
-__webpack_require__(45)
-
-var Component = __webpack_require__(0)(
- /* script */
- __webpack_require__(9),
- /* template */
- __webpack_require__(40),
- /* scopeId */
- null,
- /* cssModules */
- null
-)
-
-module.exports = Component.exports
-
-
-/***/ }),
-/* 31 */
-/***/ (function(module, exports, __webpack_require__) {
-
-var Component = __webpack_require__(0)(
- /* script */
- __webpack_require__(10),
- /* template */
- __webpack_require__(37),
- /* scopeId */
- null,
- /* cssModules */
- null
-)
-
-module.exports = Component.exports
-
-
-/***/ }),
-/* 32 */
-/***/ (function(module, exports, __webpack_require__) {
-
-var Component = __webpack_require__(0)(
- /* script */
- __webpack_require__(11),
- /* template */
- __webpack_require__(34),
- /* scopeId */
- null,
- /* cssModules */
- null
-)
-
-module.exports = Component.exports
-
-
-/***/ }),
-/* 33 */
-/***/ (function(module, exports, __webpack_require__) {
-
-var Component = __webpack_require__(0)(
- /* script */
- __webpack_require__(12),
- /* template */
- __webpack_require__(35),
- /* scopeId */
- null,
- /* cssModules */
- null
-)
-
-module.exports = Component.exports
-
-
-/***/ }),
-/* 34 */
-/***/ (function(module, exports) {
-
-module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
- return _c('div', {
- staticClass: "output"
- }, [_c('prompt'), _vm._v(" "), _c('img', {
- attrs: {
- "src": 'data:' + _vm.outputType + ';base64,' + _vm.rawCode
- }
- })], 1)
-},staticRenderFns: []}
-
-/***/ }),
-/* 35 */
-/***/ (function(module, exports) {
-
-module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
- return _c(_vm.componentName, {
- tag: "component",
- attrs: {
- "type": "output",
- "outputType": _vm.outputType,
- "count": _vm.count,
- "raw-code": _vm.rawCode,
- "code-css-class": _vm.codeCssClass
- }
- })
-},staticRenderFns: []}
-
-/***/ }),
-/* 36 */
-/***/ (function(module, exports) {
-
-module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
- return _c('div', {
- staticClass: "cell"
- }, [_c('code-cell', {
- attrs: {
- "type": "input",
- "raw-code": _vm.rawInputCode,
- "count": _vm.cell.execution_count,
- "code-css-class": _vm.codeCssClass
- }
- }), _vm._v(" "), (_vm.hasOutput) ? _c('output-cell', {
- attrs: {
- "count": _vm.cell.execution_count,
- "output": _vm.output,
- "code-css-class": _vm.codeCssClass
- }
- }) : _vm._e()], 1)
-},staticRenderFns: []}
-
-/***/ }),
-/* 37 */
-/***/ (function(module, exports) {
-
-module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
- return _c('div', {
- staticClass: "output"
- }, [_c('prompt'), _vm._v(" "), _c('div', {
- domProps: {
- "innerHTML": _vm._s(_vm.rawCode)
- }
- })], 1)
-},staticRenderFns: []}
-
-/***/ }),
-/* 38 */
-/***/ (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) {
- return _c(_vm.cellType(cell.cell_type), {
- key: index,
- tag: "component",
- attrs: {
- "cell": cell,
- "code-css-class": _vm.codeCssClass
- }
- })
- })) : _vm._e()
-},staticRenderFns: []}
-
-/***/ }),
-/* 39 */
-/***/ (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: []}
-
-/***/ }),
-/* 40 */
-/***/ (function(module, exports) {
-
-module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
- return _c('div', {
- staticClass: "cell text-cell"
- }, [_c('prompt'), _vm._v(" "), _c('div', {
- staticClass: "markdown",
- domProps: {
- "innerHTML": _vm._s(_vm.markdown)
- }
- })], 1)
-},staticRenderFns: []}
-
-/***/ }),
-/* 41 */
-/***/ (function(module, exports) {
-
-module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
- return _c('div', {
- class: _vm.type
- }, [_c('prompt', {
- attrs: {
- "type": _vm.promptType,
- "count": _vm.count
- }
- }), _vm._v(" "), _c('pre', {
- ref: "code",
- staticClass: "language-python",
- class: _vm.codeCssClass,
- domProps: {
- "textContent": _vm._s(_vm.code)
- }
- }, [_vm._v("\n ")])], 1)
-},staticRenderFns: []}
-
-/***/ }),
-/* 42 */
-/***/ (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__(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)("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?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);
- });
- }
- // When the module is disposed, remove the <style> tags
- module.hot.dispose(function() { update(); });
-}
-
-/***/ }),
-/* 43 */
-/***/ (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__(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)("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?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);
- });
- }
- // When the module is disposed, remove the <style> tags
- module.hot.dispose(function() { update(); });
-}
-
-/***/ }),
-/* 44 */
-/***/ (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__(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)("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?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);
- });
- }
- // When the module is disposed, remove the <style> tags
- module.hot.dispose(function() { update(); });
-}
-
-/***/ }),
-/* 45 */
-/***/ (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__(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)("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?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);
- });
- }
- // When the module is disposed, remove the <style> tags
- module.hot.dispose(function() { update(); });
-}
-
-/***/ }),
-/* 46 */
-/***/ (function(module, exports) {
-
-/**
- * Translates the list format produced by css-loader into something
- * easier to manipulate.
- */
-module.exports = function listToStyles (parentId, list) {
- var styles = []
- var newStyles = {}
- for (var i = 0; i < list.length; i++) {
- var item = list[i]
- var id = item[0]
- var css = item[1]
- var media = item[2]
- var sourceMap = item[3]
- var part = {
- id: parentId + ':' + i,
- css: css,
- media: media,
- sourceMap: sourceMap
- }
- if (!newStyles[id]) {
- styles.push(newStyles[id] = { id: id, parts: [part] })
- } else {
- newStyles[id].parts.push(part)
- }
- }
- return styles
-}
-
-
-/***/ }),
-/* 47 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-var Notebook = __webpack_require__(6);
-
-module.exports = {
- install: function install(_vue) {
- _vue.component('notebook-lab', Notebook);
- }
-};
-
-/***/ })
-/******/ ]);
-}); \ No newline at end of file
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/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/yarn.lock b/yarn.lock
index e16cd9c3673..fdef0665d15 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1119,6 +1119,14 @@ cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
+clipboard@^1.5.5:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53"
+ dependencies:
+ good-listener "^1.2.0"
+ select "^1.1.2"
+ tiny-emitter "^1.0.0"
+
cliui@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
@@ -1596,6 +1604,10 @@ delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+delegate@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.2.tgz#1e1bc6f5cadda6cb6cbf7e6d05d0bcdd5712aebe"
+
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -1988,6 +2000,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"
@@ -2493,6 +2509,12 @@ globby@^5.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
+good-listener@^1.2.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
+ dependencies:
+ delegate "^3.1.2"
+
got@^3.2.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca"
@@ -3550,6 +3572,10 @@ map-stream@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
+marked@^0.3.6:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7"
+
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"
@@ -3612,7 +3638,7 @@ mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7:
dependencies:
mime-db "~1.26.0"
-mime@1.3.4, mime@^1.3.4:
+mime@1.3.4, mime@1.3.x, mime@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
@@ -3684,6 +3710,10 @@ nested-error-stacks@^1.0.0:
dependencies:
inherits "~2.0.1"
+node-ensure@^0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7"
+
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"
@@ -4076,6 +4106,13 @@ pbkdf2@^3.0.3:
dependencies:
create-hmac "^1.1.2"
+pdfjs-dist@^1.8.252:
+ version "1.8.252"
+ resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-1.8.252.tgz#2477245695341f7fe096824dacf327bc324c0f52"
+ dependencies:
+ node-ensure "^0.0.0"
+ worker-loader "^0.8.0"
+
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
@@ -4411,6 +4448,12 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+prismjs@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.6.0.tgz#118d95fb7a66dba2272e343b345f5236659db365"
+ optionalDependencies:
+ clipboard "^1.5.5"
+
private@^0.1.6:
version "0.1.7"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
@@ -4869,6 +4912,10 @@ select2@3.5.2-browserify:
version "3.5.2-browserify"
resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.2-browserify.tgz#dc4dafda38d67a734e8a97a46f0d3529ae05391d"
+select@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
+
semver-diff@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
@@ -5364,6 +5411,10 @@ timers-browserify@^2.0.2:
dependencies:
setimmediate "^1.0.4"
+tiny-emitter@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb"
+
tmp@0.0.28, tmp@0.0.x:
version "0.0.28"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
@@ -5498,6 +5549,13 @@ update-notifier@0.5.0:
semver-diff "^2.0.0"
string-length "^1.0.0"
+url-loader@^0.5.8:
+ version "0.5.8"
+ resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.5.8.tgz#b9183b1801e0f847718673673040bc9dc1c715c5"
+ dependencies:
+ loader-utils "^1.0.2"
+ mime "1.3.x"
+
url-parse@1.0.x:
version "1.0.5"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"
@@ -5781,6 +5839,12 @@ wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+worker-loader@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-0.8.0.tgz#13582960dcd7d700dc829d3fd252a7561696167e"
+ dependencies:
+ loader-utils "^1.0.2"
+
wrap-ansi@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"